dafny-lang / dafny

Dafny is a verification-aware programming language
https://dafny.org
Other
2.92k stars 262 forks source link

Assertions/reveal localized in expressions #3806

Open MikaelMayer opened 1 year ago

MikaelMayer commented 1 year ago

Summary

This feature requests consists in adding a way in Dafny to ensure that some assertions are visible only within a certain scope.

Background and Motivation

Proof durability is one of the major concerns of some of our high-level Dafny customers. Basically, it all boils down to ensure that the verifier is not provided with too many facts that could blow up verification time. One very practical way to reduce the number of facts known to the verifier is to use opaque labelled assertions associated with reveal statements. Unfortunately, this is system is not very robust. Some efforts has been made but problems still remain (#2260, #3719, #3506, #2941, #3805 ...). One of the most emblematic ones is #1382, that indicates that revealing functions inside functions extends past the function definition itself.

We need better mechanisms to make things opaque and not blow up

Context

We cannot just state that assertions don't leak outside of their scope. In Dafny, assertions and reveals inside blocks always "leak" to outer scopes, and for good reasons. It makes it possible to develop appropriate proofs in different assumption contexts.

So, whereas the following works

predicate P()
method Test(b: bool)
  requires p: b <==> P()
{
  if b {
    assert P() by { reveal p; }
  }
  assert b ==> P(); // Can be proved even if p is not visible
  assert P() ==> b; // Cannot be proved
}

Which poses the question: Don't we want the same for functions? Since in functions, there is no "after" an if statement, the best equivalent we can think of is an assignment:

predicate P()
function Test(b: bool): int
{
  var x := if b then
      assert P() by { assume b <==> P(); }
      15 else 16;
  assert b ==> P(); // Can be proved even if the assumption is not visible
  assert P() ==> b; // Cannot be proved
  x
}

And indeed this one works the same. Hence if we want assertions/reveal that are local in their block, we need to either do one of these two things:

In the following alternatives, we are going to only consider at first ways to make assertions in expressions local in their block.

Alternative A: Redesign the semantics of an existing language feature

  1. We keep the same semantics for assertion assumptions and reveal in statements.
  2. In expressions, assert, reveal and assume are only visible to the expressions that they are next to. However, the assumptions of the expressions themselves are still visible from the outside. In a sense, an expression like assert A; x is the same as "assume PrecondX; x", + an unrelated proof of "assert A; assume A; assert PrecondX" where PreCondX is the precondition to be able to build X.

So for example,

opaque function F(x: int): nat {
  1
}

function AllArgumentsNotZero(x: int, y: int): int
  requires x != 0
  requires y != 0

function testCallF(): int {
    var r := (reveal F(); AllArgumentsNotZero(F(0), F(1)));
    assert F(0) != 0; // Always verified
    assert F(1) != 0; // Always verified
    assert F(0) == 1; // Used to verify, should not verify anymore
    assert F(1) == 1; // Used to verify, should not verify anymore
    r
}

This should be the same for assume and assert StmtExpr.

Pro

Cons

Alternative B: New prefix language construct

We could repurpose a keyword so that Alternative A is implemented but only for assert/reveal/assume prefixed with that keyword, e.g.

Pro

Cons

Alternative C: Season assert/assume/reveal with attribute

We don't reinvent a keyword for now, but we introduce an attribute..

Pro

Cons

Alternative D: Expand the assert statement

We could invent the following expression and statement

assert X within { expression or statements }
assert X by { Proof } within { expression or statements }
assume X within { expression or statements }
reveal a, b within { expression or statements } 

Pro

Cons

Alternative E. Invent a new "by" construct.

An expression E could be affixed with by { Proof } in which case all assert/assume/reveal in Proof are accessible to prove E, but not outside of the entire expression. It could be prefixed or suffixed:

E by { proof }
by { proof } E

    // for example:
    var r := AllArgumentsNotZero(F(0), F(1)) by {reveal P(); };
    var r := by { reveal P();} AllArgumentsNotZero(F(0), F(1));

Pro

Cons

Why bothering?

Not having this feature prevents some Dafny customers to scale up their designs and their proofs. Workaround include more refactoring, which is not always practical or desirable, or increasing the number of cores.

Other test cases to consider:

opaque predicate P(x: int) {
  true
}

function F(u: int): (r: int)
  ensures P(r)
{
  reveal P(); // Without annotation, assert P(2); below verifies. However, with the annotation, the assert P(2) should no longer verify.
  u
} by method {
  assert P(2); // expected error, but Dafny does not report any error
  r := u;
}

Other alternatives

Other solutions we considered included:

MikaelMayer commented 1 year ago

@atomb @alex-chew @fabiomadge and myself discussed this today. Here is the fruit of our discussion.

Decision

In a first phase, we will solve this issue by implementing the following decisions:

Notes about decision

Future considerations

After having implementing the decisions, we will be able to consider the following extensions

We will then be able to experiment on our codebase and of selected Dafny users, to determine how much it is desirable and a hassle that scoped assumptions become the default. In the case we consider making scoped assumptions the default (e.g. in Dafny 5),

If we do not consider making it the default:

keyboardDrummer commented 1 year ago

Thanks for sharing the notes! Sounds good.

In case you're looking for another opinion about how to continue after doing more experimentation, I'm in favor of the breaking change, but with the option of getting the old behavior using a CLI flag, so existing customers can use that flag for as long as they want, and new customers get the new behavior.