-
Notifications
You must be signed in to change notification settings - Fork 112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes lost dependencies for pruning during monomorphization. #767
Conversation
The fix is a draft and not optimal, for instance it sometimes adds dependencies that were not part of the `uses` clauses of some functions.
I am afraid we have to hit the brakes here. As I have indicated in the discussion on issue #560 , I don't understand parts of the implementation of pruning. This PR will leak some of that poorly-understood complexity into the implementation of monomorphization as well. I have a question and a suggestion for your consideration. First, the question: Suppose an edge is explicitly indicated from a function to an axiom via Second, the suggestion: The most difficult to understand part of the current implementation of pruning is the automatic generation of incoming edges into axioms through the analysis of triggers. I suggest we drop this feature, thereby requiring the user of Boogie to do some analysis itself and put the appropriate annotations explicitly in Boogie code. |
Are you saying it removes uses clauses from functions and constants? That seems unlikely to me. Can you clarify what you meant?
Can you provide an example that shows which uses clauses are missing in the monomorphized code? The way I understood the problem is that monomorphization creates new axioms based on existing ones, and does not copy over the uses clauses for the created ones. |
Source/Core/AST/Absy.cs
Outdated
public IList<Axiom> DefinitionAxioms { get; } | ||
public IList<Axiom> DefinitionAxioms => definitionAxioms; | ||
|
||
private IList<Axiom> definitionAxioms = new List<Axiom>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use a property+field construction instead of the existing auto property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, will add a set
to the auto property instead.
Source/Core/AST/Absy.cs
Outdated
|
||
private IList<Axiom> definitionAxioms = new List<Axiom>(); | ||
|
||
public bool RemoveDefinitionAxiom(Axiom axiom) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add these Remove and Add methods, given that they are already available through DefinitionAxioms.Remove
and DefinitionAxioms.Add
? It looks like an attempt at encapsulation but I don't think it encapsulates anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I can change to use those instead.
Source/Core/Monomorphization.cs
Outdated
program.RemoveTopLevelDeclarations(decl => decl is Axiom); | ||
program.AddTopLevelDeclarations(splitAxioms); | ||
program.AddTopLevelDeclarations(polymorphicMapDatatypeCtorDecls); | ||
Contract.Assert(MonomorphismChecker.IsMonomorphic(program)); | ||
return monomorphizationVisitor; | ||
} | ||
|
||
private static void UpdateDependencies(List<Axiom> splitAxioms, MonomorphizationVisitor monomorphizationVisitor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you have to update the uses clauses after the fact? Is it not simpler to update them immediately when the new axioms are created?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried doing exactly that initially, but there are a few reasons for doing it this way:
- New axioms are also added in different classes.
- Original axioms are actually quite a bit changed in-place at the point
new Axiom
is called (they are already possibly instantiated). What is being split is possibly a big conjunction of all axiom instantiations. To reestablish the dependencies efficiently, one needs to propagate other information down into those classes and check if the split axioms have the relevant symbols. There is also the case with axioms that are no longer quantified after instantiation (unless we turn off all automatic detection of dependencies in the pruning algorithm). - I think it might also be more resilient to changes in monomorphization code if the changes create new axioms elsewhere, as the only thing one needs to do is to add the old and new axiom to a dictionary.
Some of these points can be argued against, and it can probably be done as you suggest, but in the end I am not sure if it would be worth the effort.
Thank you both for the reviews @shazqadeer and @keyboardDrummer! I was hoping the PR would initiate exactly this kind of discussion.
I agree, there seem to be some assumptions the pruning algorithm makes, and these seem to have changed over time since its initial implementation.
This was something I was hoping to get your opinions on. Let's call the polymorphic function
There are also axioms that do not contain any polymorphic quantifiers in it, but these axioms are still split into its conjuncts. The same algorithm is applied here
I agree that this would be a good solution, and make the fix for the second issue mentioned in the PR unnecessary. It would also prevent the leak from the pruning algorithm into the monomorphization code, since I think this fix is the only part that depends on assumptions from the pruning algorithm (i.e., symbols inside triggers are considered automatically as incoming edges to axioms). However, I did not want to change anything in the pruning code since that would require changes in front-end tools that use pruning (only Dafny?). To summarize, this PR attempts to:
|
What I meant was
Here is an example from Dafny: Putting this inside the
Yes, this is the first issue mentioned in this PR. It is a bit tricky as both functions and axioms can be instantiated, and even non-instantiated axioms are split into their conjuncts during monomorphization. It would be very inefficient to add edges to all new axioms that are generated from an original axiom, as the number of new axioms can grow quickly depending on the number of polymorphic quantifiers in it. This is independent from above discussion in this comment however. |
Addresses review comments in PR boogie-org#767.
Your method for adding edges between instantiated function and instantiated axioms seems fine to me. Please make sure you document it somewhere in the Boogie code. Please reduce the scope of this PR so that it addresses only issue #1 from your PR description. Dafny is the only user of the |
@shazqadeer Thank you for the feedback, I will add some documentation to the code for (1) and reduce the scope of the PR. I discussed this with @atomb and will also look into the feasibility of removing edge inference using triggers. I think something like a One complication is that both symbol names and axioms can change during monomorphization, but I think this can be handled by updating the new axiom attributes using aforementioned method of adding edges between instantiated functions and axioms, possibly inside the proposed Edit - more clarification (mostly for myself): |
I didn't understand why both |
Each declaration can have both incoming and outgoing declarations. I was basing my understanding on this comment:
This comment is out of date. The method Here are some examples to explain the current semantics:
Here the procedure If we add axiom Since Example 2
Now the axiom is quantified and has a trigger, so automatic edge inference is applied. This builds the dependencies as: Still, triggers are more expressive than Example 3:
Here the trigger of axiom
Here
and One might at this point wonder what this expressiveness brings; one could just put both axioms inside a
Here I tried moving these axioms out of the Since |
To test my understanding, I rewrote your three examples using the Example 1:
Example 2:
Example 3:
Is it also correct that when we move to I suppose the Can we also get rid of |
I had the same idea with Examples 1 & 2. I had a slightly different idea in Example 3, but maybe this was not the best example: it cannot be verified even without pruning as the axioms don't trigger without both If
i.e., make all symbols in the trigger Contrast this to using a
In this case no pruning happens, but the program also does not verify, because
I am not sure about this, I think the algorithm remains similar? Reachability would still be computed from the roots, but with
I think it could contain any named declaration.
Yes, in axioms without triggers, |
I reduced the scope of this PR as requested. If you think adding |
Makes sense
Does this occur in practice? I wonder whether we could disallow such uses/used-by clauses. I think adding Also, I hope
I don't think anything would change there. |
Source/Core/Monomorphization.cs
Outdated
public readonly HashSet<Function> originalFunctions; | ||
public readonly HashSet<Constant> originalConstants; | ||
// Note that old axioms refer to axioms of the original program which might have been updated in-place during monomorphization. | ||
public readonly Dictionary<Axiom, HashSet<Axiom>> oldAxiomsToSplitAxioms; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to originalAxiomToSplitAxioms
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done (I also renamed the other dictionary).
Source/Core/Monomorphization.cs
Outdated
@@ -1293,11 +1297,20 @@ class MonomorphizationVisitor : VarDeclOnceStandardVisitor | |||
private HashSet<TypeCtorDecl> visitedTypeCtorDecls; | |||
private HashSet<Function> visitedFunctions; | |||
private Dictionary<Procedure, Implementation> procToImpl; | |||
public readonly HashSet<Function> originalFunctions; | |||
public readonly HashSet<Constant> originalConstants; | |||
// Note that old axioms refer to axioms of the original program which might have been updated in-place during monomorphization. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure old axioms are updated in-place? If I remember correctly, each axiom could be instantiated multiple times so the instantiation is done on a duplicate version of the axiom which is modified in-place. The original axiom stays as is to facilitate the next instantiation. Could you please check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I checked now and old axioms get instantiated in-place after executing this line. I think this comment also confirms that the old axioms are instantiated in-place (with the help of MonomorphizationDuplicator
).
Thanks for explaining the tuple notation for each member of the It would be great to see |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add tests to this PR?
Source/Core/Monomorphization.cs
Outdated
newAxiomConstants.Add(newAxiom, newAxiomFC.Constants); | ||
} | ||
|
||
void UpdateFunctionDependencies(HashSet<Function> functions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the parameter HashSet<Function> functions
seems unnecessary, since it's also available through monomorphizationVisitor.originalFunctions
I'm not always sure how much inner functions help read the code. Here I have to inspect the outer scope to understand the meaning of some of the variables used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed inner functions. I used inner functions initially because they were called twice in the original version of this PR, but after reducing its scope this changed.
Source/Core/Monomorphization.cs
Outdated
|
||
void UpdateFunctionDependencies(HashSet<Function> functions) | ||
{ | ||
foreach (var function in functions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider renaming: function
-> oldFunction
, functions
-> oldFunctions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed inner functions. I also changed old
to original
to match related dictionary names.
Source/Core/Monomorphization.cs
Outdated
oldAxiomFC.Visit(unmodifiedOldAxiom); | ||
var oldAxiomFunctions = oldAxiomFC.Functions; | ||
|
||
Dictionary<Axiom, HashSet<Function>> newAxiomFunctions = new Dictionary<Axiom, HashSet<Function>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use var
here? Does your IDE not suggest that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. My IDE was apparently suggesting that, which I didn't notice.
An axiom in DafnyPrelude was incorrectly being pruned away with the `monomorphic` type encoding, because it was losing its (polymorphic) quantifier and trigger. This meant automatic edge inference from triggers was no longer applicable. Moved the axiom in question into a uses clause to prevent it from being pruned. The change has no affect on other type encodings `arguments` (current default) and `predicates`. There is more discussion related to the fix here in [Boogie PR 767](boogie-org/boogie#767). <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [MIT license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
Source/Core/Monomorphization.cs
Outdated
public readonly HashSet<Constant> originalConstants; | ||
// Note that original axioms refer to axioms of the original program which might have been updated in-place during monomorphization. | ||
public readonly Dictionary<Axiom, HashSet<Axiom>> originalAxiomToSplitAxioms; | ||
public readonly Dictionary<Axiom, Axiom> originalAxiomToUninstantiatedCopies; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want to hold on to the original axiom so that you can walk over it and calculate some state. I am wondering if you can do this calculation and stash the computed state somewhere before the original axiom is modified in-place. It seems this approach might be more intuitive to understand.
If you choose to retain the current approach, rename to originalAxiomToUninstantiatedCopy
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this:
We add something like the following to Absy.Axiom
public Axiom OriginalAxiom;
public Axiom OriginalAxiomUninstantiatedCopy;
and update them during monomorphization. This is in line with instantiated DeclWithFormals
in Absy.cs. We can fill in OriginalAxiomUninstantiatedCopy
at entry to monomorphization, and fill in OriginalAxiom
when splitting (instantiated and uninstantiated) axioms.
We could also add to Absy.Axiom
:
public ISet<Function> Functions;
public ISet<Function> Constants;
which could return body functions & symbols for an axiom. Then we would no longer need the additional dictionaries. The function and constant collector could also be moved out somewhere, maybe to Absy.cs
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented the version I proposed above, but I am not sure if it ended up being any better. In the new implementation members of Axiom
need to be initialized in the right places during monomorphization, as opposed to adding entries to the dictionaries. The dictionaries are no longer needed, but similar collections need to be computed again from splitAxioms
inside UpdateFunctionDependencies
. Maybe additional members in Axiom
, which are only used during monomorphization, are also undesirable. Overall I am tempted to just change the name for now. This can perhaps be considered again when adding used_by
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I made the suggestion, I had something very simple in mind. You have the following lines of code in your PR:
var originalAxiomFc = new FunctionAndConstantCollector();
var uninstantiatedOriginalAxiom = monomorphizationVisitor.originalAxiomToUninstantiatedCopy[originalAxiom];
originalAxiomFc.Visit(uninstantiatedOriginalAxiom);
If I understand this correctly, you retrieving the cloned axiom (prior to in-place updates) just so you can call the originalFc.Visit
method. You could call this method up front and stash originalAxiomFc in the dictionary instead. Then you can look up the collected functions and constants later.
If you don't like this suggestion, go ahead and land your PR as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, and axioms don't need to be cloned anymore. I just pushed a commit doing this, thanks for the suggestion!
I do not know of any use cases, none in the
I will add a few tomorrow. |
I added two tests. The tests will need to be updated if I also changed slightly how
If the change to |
Test/pruning/MonomorphicSplit.bpl
Outdated
|
||
// The following checks are a bit simplistic, but this is | ||
// on purpose to reduce brittleness. We assume there would now be two uses clauses | ||
// with one axiom each, and those axioms should not be conjunctions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can an axiom be a conjunction of monomorphized functions? The types do not match. Please rephrase.
The last sentence of this paragraph is equally mysterious.
Test/pruning/MonomorphicSplit.bpl
Outdated
|
||
procedure monomorphicSplit() | ||
ensures f1(true) == 42; | ||
ensures f1(3) == 42; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason to indent the second ensures past the first one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this was not visible on my end in both Emacs and VS Code, apparently the second indentation was added as a tab (new laptop). Should be fixed now.
Test/pruning/NonmonomorphicSplit.bpl
Outdated
// axiom f1(0) == 1 and axiom f2(0) == 2. | ||
// Current implementation ensures that f1(0) == 1 is kept as a dependency | ||
// of f1, and f2(0) is moved to be a dependency of f2. If there would be | ||
// other symbols in the axiom, the new split axioms would be added to those |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sort of understand what the last sentence is saying. But I am confident its meaning can be made more transparent. It is fine to define symbols and use them later to clarify what is being said.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback, they were written a bit hastily... I tried updating both explanations based on the comments.
@keyboardDrummer About your question from earlier:
I was having a look at this paper, and I believe this is what the paper refers to as trigger sharing, and is sometimes useful. Consider Example 3 from before (ignoring pruning for now):
This does not verify, because there are no terms that can trigger any of the axioms. However, if we add
This verifies. When we add pruning and monomorphization, If |
After thinking about this more, your initial suggestion (quoted below for clarity) seems perfect.
|
@shazqadeer if you approve this, could you merge it? |
(Note that this is @zafer-esen's work. I created the PR for permissions reasons.) Dafny uses the Boogie option `prune` by default. (Introduced in [Boogie PR #427](boogie-org/boogie#427), although the semantics explained there is [out of date](boogie-org/boogie#767 (comment)).). The `prune` option, when enabled, runs a reachability analysis in Boogie, and prunes away functions, constants and axioms that are not reachable from root nodes. This greatly reduces resource counts, and leads to less brittleness; which is why pruning is intentionally made as strong as possible. The semantics of pruning, at a high level, is as follows. If an axiom is inside a `uses` clause of some symbol `s`: * without quantifiers or without triggers: do not prune if `s` is reachable. * with both quantifiers and triggers: do not prune if (a) `s` is reachable _or_ (b) all symbols in one of its triggers are reachable. If an axiom is not inside a `uses` clause: * without quantifiers or without triggers: always pruned. * with both quantifiers and triggers: do not prune if (b) all symbols in one of its triggers are reachable. If (a) holds but (b) does not, this leads to a weaker pruning if an axiom with quantifiers and triggers is inside a `uses` clause. (b) matches the semantics of how axioms with triggers are instantiated (AFAIK), so axioms with both quantifiers and triggers should not be inside `uses` clauses (with the exception of purely polymorphic quantifiers, more on this at the end of this comment). Consider the [following lines](https://github.com/dafny-lang/dafny/blob/c31932b4b4f77a38e5da9c9f6a7689d8f57346bf/Source/DafnyCore/DafnyPrelude.bpl#L230-L290) from `DafnyPrelude.bpl`: ``` function $Is<T>(T,Ty): bool uses { // no heap for now axiom(forall v : int :: { $Is(v,TInt) } $Is(v,TInt)); axiom(forall v : real :: { $Is(v,TReal) } $Is(v,TReal)); axiom(forall v : bool :: { $Is(v,TBool) } $Is(v,TBool)); axiom(forall v : char :: { $Is(v,TChar) } $Is(v,TChar)); axiom(forall v : ORDINAL :: { $Is(v,TORDINAL) } $Is(v,TORDINAL)); [...] // more axioms for every type } ``` `$Is` is a function symbol and `TInt`, `TReal` etc. are constant symbols, i.e., the axioms shown are quantified and all have triggers with two function or constant symbols. Since they are inside the `uses` clause of `$Is`, none of these axioms can be pruned if `$Is` is reachable. However, if the second symbol inside the triggers, say `TReal` in the second axiom, is still unreachable, that axiom cannot be triggered despite being preserved (but will negatively affect resource counts). This PR moves all quantified axioms with triggers outside of their `uses` clauses. The one exception is purely polymorphic quantifiers ([example](https://github.com/dafny-lang/dafny/blob/2a2e1c41af9b89c10437abc71cca92eb818e02a8/Source/DafnyCore/DafnyPrelude.bpl#L970-L972)), which should remain inside a `uses` clause, as these quantifiers disappear if monomorphization is used in Boogie. By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license. --------- Co-authored-by: Zafer Esen <[email protected]> Co-authored-by: Remy Willems <[email protected]>
@zafer-esen : There was discussion in this PR for dropping |
There was a small discussion about this recently in a Dafny PR where I wrote the following in a comment:
It would be great to know what you all think @shazqadeer, @atomb and @keyboardDrummer about this. If the edge inference from triggers infers exactly the set of axioms that can be instantiated, maybe it makes sense to keep edge inference around. With this PR merged, Another small concern is, unlike |
I think it does
Sounds good |
) An axiom in DafnyPrelude was incorrectly being pruned away with the `monomorphic` type encoding, because it was losing its (polymorphic) quantifier and trigger. This meant automatic edge inference from triggers was no longer applicable. Moved the axiom in question into a uses clause to prevent it from being pruned. The change has no affect on other type encodings `arguments` (current default) and `predicates`. There is more discussion related to the fix here in [Boogie PR 767](boogie-org/boogie#767). <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [MIT license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
An axiom in DafnyPrelude was incorrectly being pruned away with the `monomorphic` type encoding, because it was losing its (polymorphic) quantifier and trigger. This meant automatic edge inference from triggers was no longer applicable. Moved the axiom in question into a uses clause to prevent it from being pruned. The change has no affect on other type encodings `arguments` (current default) and `predicates`. There is more discussion related to the fix here in [Boogie PR 767](boogie-org/boogie#767). <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [MIT license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
This PR attempts to fix more axioms being pruned with the
/prune
flag than intended with monomorphization.Monomorphization introduces new axioms that are monomorphized, and tries splitting all axioms (polymorphic or not) into their conjuncts. This PR adds logic to update functions and constants to point to the new axioms as dependencies.