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

state machine refinement structure #2

Open
tjhance opened this issue Jul 13, 2021 · 6 comments
Open

state machine refinement structure #2

tjhance opened this issue Jul 13, 2021 · 6 comments

Comments

@tjhance
Copy link
Collaborator

tjhance commented Jul 13, 2021

Currently our refinement structure looks like: we have a bunch of state machines, A, B, C, ... etc. with refinement theorems of the form A_Refines_B.i.dfy. These are all named pretty consistently (although something much less consistent is the state machine invariants; sometimes the invariants for A are in the A file itself, and sometimes they are in separate files).

Sometimes, the A_Refines_B.i.dfy files are interesting, and sometimes they are not. There are a lot of files A_Refines_C which simply compose two existing refinements, A_Refines_B and B_Refines_C. All these 'composition' refinements have nearly the exact same boiler plate. With a "module system" on the horizon, we can start to think about writing a generic refinement composition proof. See this proof-of-concept.

However, there's another issue that needs to be taken care of first, which has to do with the way the refinement theorems are stated.

Here's a typical refinement theorem statement (Betree_Refines_Map.i.dfy):

  lemma RefinesNext(s: Betree.Variables, s':Betree.Variables, uiop: UI.Op)
    requires Inv(s)
    requires Betree.Next(s, s', uiop)
    ensures Inv(s')
    ensures MS.Next(I(s), I(s'), uiop)
  {
    // ...
  }

Note that this has Inv(s) as a pre-condition. Often, the interpretation function in these refinements has Inv as a precondition as well. The consequence of this is that A_Refines_B only composes with B_Refines_C if A.Inv implies B.Inv. This is problematic for a bunch of reasons, including:

  • It makes it impossible to write a generic composition proof within the module system (no such proof would be allowed to rely on an SMT condition like A.Inv ==> B.Inv)
  • Creates long dependency chains where invariants include other invariants that they don't really need.(*) This is bad for proof isolation.

(*) One (the only?) exception is ByteBlockCacheSystem, which includes the invariants of BlockCacheSystem for nontrivial reasons.

The solution to this is to define our generic refinement theorem without reference to 'Inv'. For example, we could use 'Reachable' instead (i.e., Reachable(s) := exists s0...sn . Init(s) && (forall i . Next(s_i, s_(i+1))) && sn == s). Reachable is the universal invariant which implies Inv for any predicate Inv satisfying the usual inductiveness conditions. We could also use an even more general definition of refinement, like behavior-based refinement. (Actually, this might be desirable for other reasons, as it would let us freely and locally introduce 'history' into state machines, which has come up in a few hypotheticals.) Either way, we need to pick some generic definition of refinement which actually composes properly, but which is implied by our usual TLA-style refinement statement. (Of course, we would define generic module-theorems that transform one style into another, so we'd keep writing proofs as before.)

@jonhnet
Copy link
Contributor

jonhnet commented Jul 13, 2021 via email

@tjhance
Copy link
Collaborator Author

tjhance commented Jul 13, 2021

you're right, you could leave a body-less lemma. But what I want, ideally, is to just instantly create the refinement tower by a giant module functor application. I don't want to have a bunch of boilerplate where I refine each module each step of the way just to instantiate a body-less lemma.

Actually, thinking more about it, there's probably another fairly simple thing to do, which is to make Inv an obligation of the refinement theorem module. (As opposed to having a single canonical Inv for the state machine.) So when you compose two refinements, you could just redeclare Inv to be whatever you need (although this is spiritually the same thing as the Reachable proposal).

@tjhance
Copy link
Collaborator Author

tjhance commented Jul 26, 2021

Here's an example of a Refinement definition that should be composable:

abstract module Refinement(ifc: Ifc, A: StateMachine(ifc), B: StateMachine(ifc)) {
  predicate Inv(s: A.Variables)

  lemma InitImpliesInv(s: A.Variables)
  requires A.Init(s)
  ensures Inv(s)

  lemma NextPreservesInv(s: A.Variables, s': A.Variables, op: ifc.Op)
  requires Inv(s)
  requires A.Next(s, s', op)
  ensures Inv(s')

  function I(s: A.Variables) : B.Variables
  requires Inv(s)

  lemma InitRefinesInit(s: A.Variables)
  requires A.Init(s)
  requires Inv(s)
  ensures B.Init(I(s))

  lemma NextRefinesNext(s: A.Variables, s': A.Variables, op: ifc.Op)
  requires Inv(s)
  requires Inv(s')
  requires A.Next(s, s', op)
  ensures B.Next(I(s), I(s'), op)
}

@parno
Copy link
Collaborator

parno commented Jul 26, 2021

In NextRefinesNext, I think you could move the Inv(s') into the ensures clause, no? It should be provable directly from NextPreservesInv. I assume it's necessary in the signature of NextRefinesNext so that in the ensures, Dafny doesn't complain about I(s'), but putting Inv(s') as the first ensures should cover that.

@tjhance
Copy link
Collaborator Author

tjhance commented Jul 26, 2021

we already have NextPreservesInv as an obligation, so it's all equivalent

@parno
Copy link
Collaborator

parno commented Jul 26, 2021

As written, any caller of NextRefinesNext will need to invoke NextPreservesInv, whereas if you make the adjustment I suggested, that wouldn't be necessary.

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

No branches or pull requests

3 participants