Statements

Stmt ::=
  | VarDecl
  | Assign
  | Reinit
  | BlockStmt
  | Call
  // assertions
  | Check
  | Assume
  | Reach
  | Assert
  | AForall
  // control flow
  | Choose
  | If
  | IfCase
  | Loop
  | LabeledStmt
  | Exit
  | Return
  // error reporting
  | Probe

The scope of a statement determines which free variables the statement may use. The scope typically includes the parameters of the enclosing procedure. It also includes local variables, according to standard lexical-scope rules. The variables in a scope are either mutable, which allows their values to be changed by statements, or immutable. A procedure’s in-parameters are immutable, whereas inout- and out-parameters are mutable.

Note

Statements and expressions in B3 are self-terminating, so there are no semi-colons.

Note

In the reference implementation of B3, parsing gives a raw AST, which is then processed into a resolved AST. The resolved AST is structured slightly differently than the raw AST that corresponds to the surface syntax. For more information, see these developer notes.

Variable declarations

VarDecl ::=
  ( var | val ) Identifier [ ":" Type ] [ "autoinv" Expression ] [ ":=" Expression ]

A declaration var x: T := e introduces a mutable local variable of type T and initial value e, and val x: T := e introduces an immutable local variable with value e.

The variable declaration is allowed to omit one of : Type or := Expression, but not both. If the type is omitted, it is determined from the given expression. If the expression is omitted, the variable is initialized with an arbitrary value of its type.

A mutable variable that is never updated is semantically equivalent to an immutable variable. However, the body of a forall statements is not allowed to declare mutable variables.

The variable remains in scope until the end of the enclosing block.

A variable is allowed to shadow previously declared variables with the same name. Doing so makes the previous variable with that name inaccessible until the shadowing variable goes out of scope.

The optional autoinv e clause associates an auto-invariant with the variable. The expression e must have type bool and can mention any variable in scope, including the variable being introduced. For a description of auto-invariants, see Auto-invariants.

Assignment statements

Assign ::=
  Identifier ":=" Expression

The statement x := e evaluates expression e and then sets variable x to have that value.

The identifier must be a mutable variable. The type of the RHS expression must be the same as the type of the LHS variable.

Re-init statements

Reinit ::=
  reinit Identifier+,

The statement reinit x, y, z sets the variables x, y, and z to arbitrary values of their types.

Each of the given identifiers must denote a mutable variable in scope. The list of variable is allowed to contain duplicates, but the effect of the statement is the same as if the duplicates are removed.

Block statements

BlockStmt ::=
  "{" Stmt* "}"

A block statement introduces a scope. Variables declared inside the block statement remain in scope until the end of the block.

Call statement

Call ::=
  Identifier "(" CallArgument ")"
CallArgument ::=
  | Expression
  | ( "inout" | "out" ) Identifier

A call statement P(e, inout y, out z) invokes procedure P, passing it the value of e as an in-parameter, variable y as an inout-parameter, and variable z as an out-parameter.

For readability, the parameter mode (in, inout, or out) of each argument is explicitly repeated at the call site[1] (where the absence of inout and out indicates an in-argument).

The identifier must denote a procedure in the program. The types of the actual arguments must be the same as for the corresponding formal parameters for the procedure.

The variables given as inout- and out-arguments must be distinct mutable variables.

It is an error if a program trace reaches the call statement and one of the callee’s preconditions does not hold. Nevertheless, a program trace proceeds into the call even if some precondition does not hold. For a deductive verifier, this is the “check and forget” semantics.

Check statements

Check ::=
  check Expression

The statement check e declares the intention that every program trace leading to this statement find e to evaluate to true. It is an error if a program trace reaches the statement and e evaluates to false.

The given expression must have type bool.

A program trace continues after the check statement even if the condition does not hold. For a deductive verifier, this is the “check and forget” semantics.

Assume statements

Assume ::=
  assume Expression

The statement assume e declares that a program trace leading to this statement is of interest only when e evaluates to true. In other words, the assume statement says that “what follows this statement is relevant only if e evaluates to true”.

The given expression must have type bool.

Reach statements

Reach ::=
  reach Expression

The statement reach e declares the intention that there exist a program trace that leads to this statement and finds e to evaluate to true.

The given expression must have type bool.

Assert statements

Assert ::=
  assert Expression

A statement assert e is equivalent to the statement sequence check e assume e.

The given expression must have type bool.

Forall statements

AForall ::=
  forall Identifier ":" Type
  BlockStmt

TODO: description

Choose statements

Choose ::=
  choose BlockStmt ( or BlockStmt )*

A statement choose S or S' arbitrarily chooses one of the alternatives S and S' and proceeds as it. In other words, the choose statement gives rise to the union of the program traces of each of its components.

If statements

If ::=
  if Expression BlockStmt [ else IfContinuation ]
IfContinuation ::=
  | BlockStmt
  | If
  | IfCase

The statement

if e
  S
else
  S'

chooses S or S' depending on the value of e. In particular, it chooses S if e evaluates to true and otherwise chooses S'.

The expression must have type bool.

If an explicit else branch is omitted, it defaults to else { }.

The three variations of IfContinuation gives a streamlined syntax for writing cascading if statements, but are semantically equivalent to putting the If or IfCase inside a block statement.

If-case statements

IfCase ::=
  if Case+
Case ::=
  case Expression BlockStmt

The statement

if
case e0 S0
case e1 S1
case e2 S2

chooses a case alternative whose expression evaluates to true and then proceeds as the corresponding statement body. It also declares that a program trace leading to this if-case statement is of interest only when at least one of the case expressions evaluates to true.

Each expression must have type bool.

The cases are unordered. If the expression for more than one case evaluates to true, the statement arbitrarily chooses one of them.

If no case evaluates to true, then the entire statement is like assume false.

Example

Here is an example that shows the if, if case, and choose statements in action.

Suppose a source language declares a type

datatype Fruit = Apricot(w: int) | Banana(w: int, y: int) | Clementine(z: int)

These may be encoded into B3 as

type Fruit
tagger FruitTag for Fruit

function Apricot(injective w: int): Fruit tag FruitTag
function Banana(injective x: int, injective y: int): Fruit tag FruitTag
function Clementine(injective z: int): Fruit tag FruitTag

Consider a source-language variable fruit: Fruit and a source-language statement

match fruit
case Apricot(w) =>
  // A...
case Banana(w, y) =>
  // B...
case Clementine(z) =>
  // C...

An encoding of such a match statement may have the following structure:

if exists w: int pattern Apricot(w) fruit == Apricot(w) {
  // ...
} else if exists x: int, y: int pattern Banana(x, y) fruit == Banana(x, y) {
  // ...
} else if exists z: int pattern Clementine(z) fruit == Clementine(z) {
  // ...
} else {
  assert false
}

where each branch has a form like

  val w: int
  assume fruit == Apricot(w)
  // A...

In such an encoding, the A... branch essentially has two variables for each source-language bound variable: a Skolemized version of exists w and the local variable w. This may in turn give rise to twice the number of quantifier instantiations, one for each of these variables.

A more complicated encoding that removes that redundancy is

choose {
  val w: int
  assume fruit == Apricot(w)
  // A...
} or {
  assume forall w: int pattern Apricot(w) fruit != Apricot(w)
  choose {
    val x: int
    val y: int
    assume fruit == Banana(x, y)
    // B...
  } or {
    assume forall x: int, y: int pattern Banana(x, y) fruit != Banana(x, y)
    choose {
      val z: int
      assume fruit == Clementine(z)
      // C...
    } or {
      assume forall z: int pattern Clementine(z) fruit != Clementine(z)
      assert false
    }
  }
}

If one can syntactically determine in the source language that all possibilities are covered, then the final branch that ends with assert false is not needed. Moreover, if the order of evaluation among the cases does not matter (for example, if the cases are known to be mutually exclusive), then the quantified assumptions in the or branches are not needed. The encoding into B3 can then be simplified to

choose {
  val w: int
  assume fruit == Apricot(w)
  // A...
} or {
  val x: int
  val y: int
  assume fruit == Banana(x, y)
  // B...
} or {
  val z: int
  assume fruit == Clementine(z)
  // C...
}

The various if and choose statements in B3 do not themselves include variable declarations.[2] Therefore, the bound variables w, x, y, and z from the source-language cases need to be declared either inside each branch or before all the branches. It’s nicer to declare them inside each branch, because then the resulting B3 scopes will correspond to those in the source language. However, if declaring all of them ahead of the branches is a possibility, then the encoding into B3 can be written as

val w: int
val x: int
val y: int
val z: int
if
case fruit == Apricot(w) {
  // A...
}
case fruit == Banana(x, y) {
  // B...
}
case fruit == Clementine(z) {
  // C...
}

Loops

Loop ::=
  loop Invariant* BlockStmt
Invariant ::=
  invariant AExpression

A loop statement

loop
  invariant e
Body

checks the condition e (like a check e) and then proceeds as Body. If a trace reaches the end of { Body }, it proceeds as the entire loop statement. Stated differently, the loop statement repeatedly proceeds as Body, checking condition e before each such iteration, and ending only if an iteration of Body ever exit abruptly (with an exit or return).

Expression e is called a loop invariant because it declares the intention that e hold at the top of every iteration of Body.

Example

A loop while b { Body() } commonly found in programming languages is encoded in B3 as

loop {
  if !b {
    exit
  }
  Body()
}

A loop do { Body() } while b is encoded as

loop {
  Body()
  if !b {
    exit
  }
}

Labeled statements

LabeledStmt ::=
  Identifier ":" ( BlockStmt | Loop )

A loop or block statement S can be given a name lbl by writing lbl: S. Such a label provides a way to abruptly exit statement S by name. More precisely, lbl: S proceeds as S, but if S exits abruptly to label lbl, then lbl: S continues normally.

In other words, lbl: S is like an empty “exception handler” for lbl in S.

A label is not allowed to shadow another label. That is, in lbl: S, statement S is not allowed to use lbl as a label. It is, however, legal to reuse the same label name in non-nested ways; for example,

lbl: S
lbl: S'

is legal.

Exit statements

Exit ::=
  exit [ Identifier ]

A statement exit lbl instigates an abrupt exit. This causes a program trace to continue immediately after the enclosing statement labeled with lbl.

The given identifier must be a label name in scope. That is, exit lbl can only be used inside a statement S in a labeled statement lbl: S.

If the exit statement is placed in the body of a (labeled or unlabeled) loop, then the label identifier can be omitted. This causes the abrupt exit to continue immediately after the most tighly enclosing (labeled or unlabeled) loop. This is especially useful when encoding common while loops, see Loops.

Example

The continue statement found in many programming languages can be encoded by labeling the body of a loop. For example, a loop like

while b {
  Stmt0()
  if c {
    continue
  }
  Stmt1()
}

is encoded as

loop {
  ContinueLabel: {
    if !b {
      exit
    }
    Stmt0()
    if c {
      exit ContinueLabel
    }
    Stmt1()
  }
  // the "exit ContinueLabel" arrives here, just before the end of the iteration
}

Example

Exception handling can also be encoded using labels and exists. For example, consider a program

try {
  Stmt0()
  if c {
    throw Unexpected
  }
  Stmt1()
} catch Unexpected {
  Handler()
}

Here, the handler for the throw statement can be determined lexically. This can be encoded in B3 as

TryBlock: {
  TryBody: {
    Stmt0()
    if c {
      exit TryBody
    }
    Stmt1()
    exit TryBlock // skip the handler
  }
  Handler()
}

In the more common case, where a handler is not in the same lexical scope as the throw, use an out-parameter to communicate if a procedure returns normally or exceptionally, and if so, with which exception. Then, inspect this variable after each call and follow the template above to transfer control to the enclosing exception handler. If there is no lexically enclosing exception handler, set the out-parameter and abruptly return (using return) from the procedure.

Return statements

Return ::=
  return

A return statement exits a procedure body abruptly. This transfers control out of the enclosing procedure body, at which point the procedure’s postconditions are checked.

A return statement can only be used in a procedure body. For example, it cannot be used inside an AExpression.

Probe statements

Probe ::=
  probe Expression

A statement probe e has no effect in a trace. It is used to record the value of expression e, in the event that an error is reported along a trace that passes through the probe statement. In this way, the probe statement is a bit like a debug print statement.[3]

The type of the expression must be bool.