Skip to content
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

fix: Cross-modular override check #3223

Merged
merged 21 commits into from
Jan 25, 2023

Conversation

fabiomadge
Copy link
Collaborator

@fabiomadge fabiomadge commented Dec 20, 2022

Fixes #2500.

To achieve this, we first give the override axiom the visibility of the overriding function. To ensure we only use the axiom in case we pass the check, we introduce an intermediate visibility level. The ensures clause of the overridden function can reference the underlying function, so we need to substitute the overridden function by the overriding function in the override check.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

Copy link
Collaborator

@RustanLeino RustanLeino left a comment

Choose a reason for hiding this comment

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

The fix looks good. Please add some test cases.

@@ -213,7 +213,7 @@ public partial class Translator {
AddWellformednessCheck(cf);
if (InVerificationScope(cf)) {
var etran = new ExpressionTranslator(this, predef, f.tok);
heightAntecedent = Bpl.Expr.Lt(Bpl.Expr.Literal(cf.EnclosingModule.CallGraph.GetSCCRepresentativePredecessorCount(cf)), etran.FunctionContextHeight());
heightAntecedent = Bpl.Expr.Lt(Bpl.Expr.Literal(cf.EnclosingModule.CallGraph.GetSCCRepresentativePredecessorCount(cf) * 2 + 1), etran.FunctionContextHeight());
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest making a SCCPredecessorCountToFunctionHeight method that performs the ... * 2 + 1 computation. And I guess the + 1 should then also be controlled by a boolean, like you're doing in HeightContext, etc.

Comment on lines 948 to 949
//adding assume J.F(ins) == C.F(ins);
//assert Post’;
Copy link
Collaborator

Choose a reason for hiding this comment

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

These two should be in the other order. That is, it should first check that the postcondition of the trait holds, and only then should it be given the information J.F(ins) == C.F(ins).

Copy link
Collaborator Author

@fabiomadge fabiomadge Dec 20, 2022

Choose a reason for hiding this comment

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

The comment is outdated. There is no assume anymore. (At least not of that equality.)

Contract.Requires(m != null);
// free requires fh == FunctionContextHeight;
var module = m.EnclosingModule;
Boogie.Expr context =
Boogie.Expr.Eq(Boogie.Expr.Literal(module.CallGraph.GetSCCRepresentativePredecessorCount(m)), FunctionContextHeight());
Boogie.Expr.Eq(Boogie.Expr.Literal(module.CallGraph.GetSCCRepresentativePredecessorCount(m) * 2 + (reducedScope ? 0 : 1)), FunctionContextHeight());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here's another place for that ... * 2 + 0-or-1 method.

Bpl.Expr useViaContext = !InVerificationScope(f) ? Bpl.Expr.True :
(Bpl.Expr)Bpl.Expr.Neq(Bpl.Expr.Literal(mod.CallGraph.GetSCCRepresentativePredecessorCount(f)), etran.FunctionContextHeight());
// useViaContext: fh < FunctionContextHeight
var lowerBound = (f.EnclosingClass.EnclosingModuleDefinition.CallGraph.GetSCCRepresentativePredecessorCount(f) + 1) * 2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

And here, too. In this instance, it would make it clear that the + 1 is indeed placed in the right place.

@@ -2254,15 +2253,15 @@ class Specialization {
return (olderParameterCount, olderCondition);
}

Bpl.Expr AxiomActivation(Function f, ExpressionTranslator etran) {
Bpl.Expr AxiomActivation(Function f, ExpressionTranslator etran, bool hidden = false) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't find hidden to be such a good description. Can you make the name more descriptive? Or, if not, add a comment that describes it.

Comment on lines 3537 to 3513
builder.Add(TrAssumeCmd(f.tok, Bpl.Expr.Eq(funcExpC, resultVar)));
builder.Add(TrAssumeCmd(f.tok, Expr.Eq(funcExpC, resultVar)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know Rider keeps saying the Bpl. is not necessary. I guess it's fine to get rid of it, but I've been fine with seeing the prefix, since it reminds me that Bpl.Expr is not Expression.

Comment on lines 3518 to 3520
var receiver = new ImplicitThisExpr(f.tok) { Type = Resolver.GetThisType(f.tok, (TopLevelDeclWithMembers)f.EnclosingClass) };
var sub = new FunctionCallSubstituter(receiver, substMap, typeMap, f.OverriddenFunction, f);
Expression postcond = sub.Substitute(en.E);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this replaces too many occurrences of the function. For example, I suspect it will make the following override verify, but it mustn't:

trait Tr {
  function F(x: nat, other: Tr): int
    ensures x != 0 && other.F(x - 1, other) < 20 ==> F(x, other) % 2 == other.F(x - 1, other) % 2
}

class Cl extends Tr {
  function F(x: nat, other: Tr): int
    ensures 50 <= F(x, other)
}

Note that the calls other.F(x - 1, other) should not be changed by the substitution.

var lowerBound = (f.EnclosingClass.EnclosingModuleDefinition.CallGraph.GetSCCRepresentativePredecessorCount(f) + 1) * 2;
Expr useViaContext = !InVerificationScope(f) ? Bpl.Expr.True : Expr.Le(Expr.Literal(lowerBound), etran.FunctionContextHeight());
var visibilityLevel = f.EnclosingClass.EnclosingModuleDefinition.CallGraph.GetSCCRepresentativePredecessorCount(f);
Expr useViaContext = !InVerificationScope(f) ? Bpl.Expr.True : Expr.Lt(MkFunctionHeight(visibilityLevel), etran.FunctionContextHeight());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good. I like this ... * 2 + 1 < ... better than the (... + 1) * 2 <= ....


module A {
trait {:termination false} Trait {
function TotallyNotZero() : (ret: int)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also add some tests that use the function itself (rather than the named result value ret), since those cases may be tricky.

@fabiomadge fabiomadge changed the title fix: cross-modular override check fix: Cross-modular override check Dec 28, 2022
@fabiomadge fabiomadge marked this pull request as ready for review January 5, 2023 02:46
Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

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

Great simple solution, and appreciate the helpful comments and code cleanup along the way!

I assume we don't need the same solution for pre-conditions as well, since the equality axiom comes after the assertion in that case?

@@ -62,19 +62,19 @@ class D extends T {
class E extends T {
// d does not name the result in either trait or class
function method d(y: nat): nat
ensures d(y) < 20 + y
ensures d(y) < 20 + y // error: specification is not weaker
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ensures d(y) < 20 + y // error: specification is not weaker
ensures d(y) < 20 + y // error: specification is weaker

And likewise throughout - unless I'm confused this was already wrong in the original comments you copied.


class Class extends M.Tr {
function True(): (ret: bool)
// Missing: ensures ret != 0
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Missing: ensures ret != 0
// Missing: ensures ret

@@ -258,12 +258,11 @@ public ExpressionTranslator(ExpressionTranslator etran, string modifiesFrame)
return new Boogie.IdentifierExpr(Token.NoToken, "$FunctionContextHeight", Boogie.Type.Int);
}

public Boogie.Expr HeightContext(ICallable m) {
public Boogie.Expr HeightContext(ICallable m, bool reducedScope = false) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: consider using "intermediateScope" here instead, or renaming the parameter, for consistency.

var receiver = Substitute(e.Receiver);
var newArgs = SubstituteExprList(e.Args);
var newFce = new FunctionCallExpr(e.tok, e.Name, receiver, e.OpenParen, e.CloseParen, newArgs, e.AtLabel);
if (e.Function == A && e.Receiver is ThisExpr && receiver is ThisExpr) {
newFce.Function = B;
newFce.Type = e.Type; // TODO: this may not work with type parameters.
Copy link
Member

Choose a reason for hiding this comment

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

Still a TODO or have you fixed it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Neither? I couldn't find a situation where it affects the way I use it, Boogie has more shallow types, but there may be others, where getting this right is crucial.

Comment on lines +20 to +23
function Other(x: nat, free: M.Tr): bool
// Different receiver
ensures x != 0 && Other(x-1, free) ==> Other(x-1, free)
{ false }
Copy link
Member

Choose a reason for hiding this comment

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

I'd add a comment about how this is a negative test of the limitations of the current substitution approach.

Also this is fine but it would be good to have a version that's "obviously true" to humans as well, to be clearer about the limitation, like:

module M {
  trait {:termination false} Tr {
    ...
    function True'': (ret: bool) ensures True''()
  }
}

class Class extends M.Tr {
  ...
  function True'': (ret: bool) ensures (var alsoThis := this; alsoThis.True''())
  { true }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a positive test, as the ensures is a tautology. If we weren't careful when lowering Other the, the override check could pass. I did add a version of your suggestion nevertheless and it uncovered an issue.

@robin-aws robin-aws enabled auto-merge (squash) January 25, 2023 18:11
@robin-aws robin-aws enabled auto-merge (squash) January 25, 2023 18:11
@robin-aws robin-aws merged commit 88ef30b into dafny-lang:master Jan 25, 2023
fabiomadge added a commit to fabiomadge/dafny that referenced this pull request Jan 31, 2023
fabiomadge added a commit that referenced this pull request Jan 31, 2023
This reverts PR #3223. It introduced an completeness issue.
fabiomadge added a commit to fabiomadge/dafny that referenced this pull request Jan 31, 2023
Fixes 2500.

To achieve this, we first give the override axiom the visibility of the
overriding function. To ensure we only use the axiom in case we pass the
check, we introduce an intermediate visibility level. The ensures clause
of the overridden function can reference the underlying function, so we
need to substitute the overridden function by the overriding function in
the override check.

Co-authored-by: Robin Salkeld <[email protected]>
atomb pushed a commit to atomb/dafny that referenced this pull request Feb 1, 2023
Fixes 2500.

To achieve this, we first give the override axiom the visibility of the
overriding function. To ensure we only use the axiom in case we pass the
check, we introduce an intermediate visibility level. The ensures clause
of the overridden function can reference the underlying function, so we
need to substitute the overridden function by the overriding function in
the override check.

Co-authored-by: Robin Salkeld <[email protected]>
atomb pushed a commit to atomb/dafny that referenced this pull request Feb 1, 2023
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.

{:termination false} also allows incorrect implementations of trait functions
3 participants