Examples

Here is a simple B3 program to whet your appetite.

// Here is a little comment

type List
tagger ListTag for List

function Nil(): List tag ListTag
function Cons(injective head: int, injective tail: List): List tag ListTag

function Length(list: List): int
axiom forall list: List pattern Length(list) 0 <= Length(list)

procedure TestLists(list: List) {
  var n := Length(list)
  check n != -9
}

A to-do item for this documentation is to also make inline code, like check x < y, be processed by pygments syntax highlighting.

Here is a longer example:

// This example program shows many B3 features in use. The main point is to prove
// that GetPublicAccessBlock is called with a bucket name that satisfies UserOwnsBucket.
// This properties is guaranteed by the postcondition of ListBuckets, which, upon
// success, returns a sequence where every bucket name satisfies GetPublicAccessBlock.
//
// Here is the program shown in the syntax of a Dafny-like programming-language:
//
//     var fileSystem :- CreateClient("myFileSystemName")
//     var aresponse :- fileSystem.ListBuckets()
//     var buckets := aresponse.buckets
//     for i := 0 to |buckets| {
//       var bucket := buckets[i]
//       var bucketName := bucket.name
//       var bresponse :- fileSystem.GetPublicAccessBlock(bucketName)
//       var isBlocked := bresponse.getConfig.GetAttributeValue("BlockPublicAcls")
//       if isBlocked {
//         print "bucket", bucketName, "is blocked"
//       } else {
//         print "bucket", bucketName, "is not blocked"
//       }
//      }
//
// Note that B3 identifiers may contain "." characters. B3 uses ".." as part of the
// name when it generates functions (for example, the function names generated as a result
// of declaring a parameter to be "injective").

procedure Good(out result: XResult) {
  var cresult: CResult
  CreateClient(|myFileSystemName: string|, out cresult)
  if !CIsSuccess(cresult) {
    result := XFailure(CFailure..msg(cresult))
    return
  }
  var fileSystem := CSuccess..value(cresult)

  var aresult: AResult
  ListBuckets(fileSystem, out aresult)
  if !AIsSuccess(aresult) {
    result := XFailure(AFailure..msg(aresult))
    return
  }
  var aresponse := ASuccess..value(aresult)

  var buckets := AResponse..buckets(aresponse)

  var i := 0
  loop
    invariant 0 <= i && i <= length(buckets)
  {
    if i == length(buckets) {
      exit
    }
    
    check 0 <= i && i < length(buckets)
    var bucket := select(buckets, i)

    var bucketName := Bucket..name(bucket)

    var bresult: BResult
    GetPublicAccessBlock(fileSystem, bucketName, out bresult)
    if !BIsSuccess(bresult) {
      result := XFailure(BFailure..msg(bresult))
      return
    }
    var bresponse := BSuccess..value(bresult)
 
    var isBlocked := GetAttributeValue(BResponse..getConfig(bresponse), |BlockPublicAcls: string|)

    if isBlocked {
      Print(|bucket: string|, bucketName, |is-blocked: string|)
    } else {
      Print(|bucket: string|, bucketName, |is-not-blocked: string|)
    }

    i := i + 1
  }

  var x: X
  result := XSuccess(x)
}

// --------------------------------------------------------------------

// The file-system API is the following:

procedure CreateClient(name: string, out result: CResult)

// This predicate says whether or not the given bucket name is owned by the user.
// It is used in the postcondition of ListBuckets and in the precondition of
// GetPublicAccessBlock.
function UserOwnsBucket(name: string): bool

type Client

procedure ListBuckets(c: Client, out aresult: AResult)
  ensures AIsSuccess(aresult) ==>
    forall bucket: Bucket
      pattern Bucket..name(bucket)
      pattern in(bucket, AResponse..buckets(ASuccess..value(aresult)))
      in(bucket, AResponse..buckets(ASuccess..value(aresult))) ==>
      UserOwnsBucket(Bucket..name(bucket))

procedure GetPublicAccessBlock(c: Client, Bucket: string, out result: BResult)
  requires UserOwnsBucket(Bucket)

// --------------------------------------------------------------------

// The example program uses an API model where each API entry point returns a "response".
// This is typical in many code bases, for example in Java, because it allows the API
// to evolve to add more attributes of the response in the future. What that means for
// the example program is that there are different response types. Here, those are modeled
// as records with one just field.

// datatype AResponse = AResponse(buckets: BucketSeq)
type AResponse
function AResponse(injective buckets: BucketSeq): AResponse

// datatype BResponse = BResponse(getConfig: BlockConfig)
type BResponse
function BResponse(injective getConfig: BlockConfig): BResponse

// --------------------------------------------------------------------

// For the purpose of the example, a bucket is something that has a name. In a full API,
// buckets would also have some data, of course.

// datatype Bucket = Bucket(name: string)
type Bucket
function Bucket(injective name: string): Bucket

// --------------------------------------------------------------------

// In the example, a block configuration is a set of named attributes, each of which can
// be false or true.

type BlockConfig

function GetAttributeValue(config: BlockConfig, attribute: string): bool

// --------------------------------------------------------------------

// The example program is written in the style of Rust, Go, and Dafny, where a failure
// is reported as a special return value that have to be tested by the caller. In Go,
// such querying and propagation of failures is done manually, whereas Rust has the
// "?" operator and Dafny has the ":-" operator for doing this. Such code is translated
// into B3 by doing the checking explicitly.
//
// Using datatypes with type parameters, such Result types can be defined as
//
//     datatype Result<X> = Success(value: X) | Failure(msg: string)
//
// Since B3 does not support polymorphism, there is one Result type for each success
// type.

type X
type XResult // Result<()>
tagger XResultTag for XResult
function XSuccess(injective value: X): XResult tag XResultTag
function XFailure(injective msg: string): XResult tag XResultTag
function XIsSuccess(r: XResult): bool {
  XResultTag(r) == XSuccess..tag()
}

type CResult // Result<Client>
tagger CResultTag for CResult
function CSuccess(injective value: Client): CResult tag CResultTag
function CFailure(injective msg: string): CResult tag CResultTag
function CIsSuccess(r: CResult): bool {
  CResultTag(r) == CSuccess..tag()
}

type AResult // Result<AResponse>
tagger AResultTag for AResult
function ASuccess(injective value: AResponse): AResult tag AResultTag
function AFailure(injective msg: string): AResult tag AResultTag
function AIsSuccess(r: AResult): bool {
  AResultTag(r) == ASuccess..tag()
}

type BResult // Result<BResponse>
tagger BResultTag for BResult
function BSuccess(injective value: BResponse): BResult tag BResultTag
function BFailure(injective msg: string): BResult tag BResultTag
function BIsSuccess(r: BResult): bool {
  BResultTag(r) == BSuccess..tag()
}

// --------------------------------------------------------------------

// Finally, we have a type BucketSeq that models a sequence of buckets
// and a(n uninterpreted) string type whose values can be printed.
//
// In a source programming language, the "select" operation on a sequence
// has a precondition that the given index is in range. The example B3 code
// above uses a "check" statement to enforce that precondition.

type BucketSeq

function select(s: BucketSeq, i: int): Bucket

function length(s: BucketSeq): int

axiom explains length
  forall s: BucketSeq
    pattern length(s)
    0 <= length(s)

function in(b: Bucket, s: BucketSeq): bool {
  exists i: int
    pattern select(s, i)
    0 <= i && i < length(s) && select(s, i) == b
}

type string

procedure Print(a: string, b: string, c: string)