• Top
    • Documentation
    • Books
    • Boolean-reasoning
    • Projects
    • Debugging
    • Std
    • Proof-automation
    • Macro-libraries
      • B*
        • B*-binders
        • Def-b*-binder
      • Defunc
      • Fty
      • Apt
      • Std/util
      • Defdata
      • Defrstobj
      • Seq
      • Match-tree
      • Defrstobj
      • With-supporters
      • Def-partial-measure
      • Template-subst
      • Soft
      • Defthm-domain
      • Event-macros
      • Def-universal-equiv
      • Def-saved-obligs
      • With-supporters-after
      • Definec
      • Sig
      • Outer-local
      • Data-structures
    • ACL2
    • Interfacing-tools
    • Hardware-verification
    • Software-verification
    • Math
    • Testing-utilities
  • Macro-libraries

B*

The b* macro is a replacement for let* that adds support for multiple return values, mixing control flow with binding, causing side effects, introducing type declarations, and doing other kinds of custom pattern matching.

Introduction

To use b* you will need to load the following book:

(include-book "std/util/bstar" :dir :system)

In its most basic form, the b* macro is nearly a drop-in replacement for let*. For instance, these are equivalent:

(let* ((x 1)               (b* ((x 1)
       (y 2)          ==        (y 2)
       (z (+ x y)))             (z (+ x y)))
  (list x y z))              (list x y z))

But beyond simple variable bindings, b* provides many useful, extended b*-binders. A simple example is the mv binder, which can nicely avoid switching between let* and mv-let. For instance:

(let* ((parts (get-parts args)))            (b* ((parts (get-parts args))
  (mv-let (good bad)                   ==       ((mv good bad) (split-parts parts))
    (split-parts parts)                         (new-good (mark-good good))
    (let* ((new-good (mark-good good))          (new-bad  (mark-bad bad)))
           (new-bad  (mark-bad bad)))         (append new-good new-bad))
      (append new-good new-bad))))

Another example is the when binder, which allows for a sort of "early exit" from the b* form without needing to alternate between let* and if. For instance:

(let* ((sum (get-sum (car x))))       (b* ((sum (get-sum (car x)))
  (if (< sum limit)               ==       ((when (< sum limit))
      ans                                   ans)
    (let* ((ans   (+ ans sum))             (ans   (+ ans sum))
           (limit (+ limit 1)))            (limit (+ limit 1)))
      (fn (cdr x) ans limit))))         (fn (cdr x) ans limit))

The only part of the let* syntax that is not available in b* is the declare syntax. However, ignore/ignorable declarations are available using a different syntax (see below), and type-spec declarations are available using the the binder.

General Form

The general syntax of b* is:

(b* <list-of-bindings> . <list-of-result-forms>)

where a result form is any ACL2 term, and a binding is

(<binder-form> [<expression>])

Depending on the binder form, it may be that multiple expressions are allowed or only a single one.

The std/util/bstar book comes with several useful b* binders already defined, which we describe below. You can also define your own, custom binder forms to extend the syntax of b* to provide additional kinds of pattern matching or to implement common coding patterns. For example, the std::defaggregate macro automatically introduces new b* binders that let you access the fields of structures using a C-like employee.name style syntax.

Note: One difference between let* and b* is that b* allows multiple forms to occur in the body, and returns the value of the last form. For example:

(b* ((x 1)
     (y 2)
     (z (+ x y)))
  (cw "Hello, ")
  (cw " world!~%")
  (list x y z))

Will print Hello, world! before returning (1 2 3), whereas putting these cw statements into a let* form would be a syntax error.

Built-In B* Binders

Here is a nonsensical example that gives a flavor for the kind of b* binders that are available "out of the box."

(b* ( ;; don't forget the first open paren! (like with let*)

     ;; let*-like binding to a single variable:
     (x (cons 'a 'b))

     ;; mv binding
     ((mv y z) (return-two-values x x))

     ;; No binding: expression evaluated for side effects
     (- (cw "Hello")) ;; prints "Hello"

     ;; Binding with type declaration:
     ((the (integer 0 100) n) (foo z))

     ;; MV which ignores a value:
     ((mv & a) (return-garbage-in-first-mv y z))

     ;; Binds value 0 to C and value 1 to D,
     ;; declares (ignorable C) and (ignore D)
     ((mv ?c ?!d) (another-mv a z))

     ;; Bind V to the middle value of an error triple,
     ;; quitting if there is an error condition (a la er-let*)
     ((er v) (trans-eval '(len (list 'a 1 2 3)) 'foo state))

     ;; The WHEN, IF, and UNLESS constructs insert an IF in the
     ;; binding stream.  WHEN and IF are equivalent.
     ((when v) (finish-early-because-of v))
     ((if v)   (finish-early-because-of v))
     ((unless c) (finish-early-unless c))

     ;; Pattern-based binding using cons, where D is ignorable
     ((cons (cons b c) ?d) (must-return-nested-conses a))

     ;; Patterns based on LIST and LIST* are also supported:
     ((list a b) '((1 2) (3 4)))
     ((list* a (the string b) c) '((1 2) "foo" 5 6 7))

     ;; Alternate form of pattern binding with cons nests, where G is
     ;; ignored and F has a type declaration:
     (`(,e (,(the (signed-byte 8) f) . ,?!g))
      (makes-a-list-of-conses b))

     ;; Pattern with user-defined constructor:
     ((my-tuple foo bar hum) (something-of-type-my-tuple e c g))

     ;; Don't-cares with pattern bindings:
     ((my-tuple & (cons carbar &) hum) (something-else foo f hum))

     ;; Pattern inside an mv:
     ((mv a (cons & c)) (make-mv-with-cons))

     ) ;; also don't forget the close-paren after the binder list

  ;; the body (after the binder list) is an implicit PROGN$
  (run-this-for-side-effects ...)
  (return-this-expression .....))

Note: The built-in binders are all defined in the ACL2 package, and can be used (without package prefix) in any other package that imports the binder symbol, or with the acl2:: packge prefix anywhere. See also the note about packages under user-defined binders below.

We now give some additional details about these built-in binders. Since users can also define their own b* binders, you may wish to see b*-binders for a more comprehensive list of available binder forms.

(mv a b ...)
Produces an mv-let binding.
(cons a b)
Binds a and b to (car val) and (cdr val), respectively, where val is the result of the corresponding expression.
(er a)
Produces an ER-LET* binding.
(list a b ...)
Binds a, b, ... to (car val), (cadr val), etc., where val is the result of the corresponding expression.
(nths a b ...)
Binds a, b, ... to (nth 0 val), (nth 1 val), etc., where val is the result of the corresponding expression. This is very much like list, but may be useful when nth is disabled.
(list* a b)
`(,a . ,b)
Alternatives to the cons binder.
(the type-spec var)
Binds var to the result of the corresponding expression, and adds a declare form saying that var is of the given type-spec. You can nest the patterns inside other patterns, but var must itself be a symbol instead of a nested pattern, and type-spec must be a valid type-spec.
(if test)
(when test)
(unless test)
These forms don't actually produce bindings at all. Instead, they insert an if where one branch is the rest of the B* form and the other is the "bound" expression. For example,
(b* (((if (atom a)) 0)
     (rest (of-bindings)))
  final-expr)
expands to something like this:
(if (atom a)
    0
  (b* ((rest (of-bindings)))
    final-expr))
These forms can also create an "implicit progn" with multiple expressions, like this:
(b* (((if (atom a))
      (cw "a is an atom, returning 0")
      0)
     ...)
  ...)

Note that the cons, list, list*, and backtick binders may be nested arbitrarily inside other binders. User-defined binders may often be arbitrarily nested. For example,

((mv (list `(,a . ,b)) (cons c d)) <form>)

will result in the following (logical) bindings:

  • a bound to (car (nth 0 (mv-nth 0 <form>)))
  • b bound to (cdr (nth 0 (mv-nth 0 <form>)))
  • c bound to (car (mv-nth 1 <form>))
  • d bound to (cdr (mv-nth 1 <form>))

Side Effects and Ignoring Variables

The following constructs may be used in place of variables

- Dash (-), used as a top-level binding form, will run the corresponding expressions (in an implicit progn) for side-effects without binding its value. Used as a lower-level binding form, it will cause the binding to be ignored or not created.
& Ampersand (&), used as a top-level binding form, will cause the corresponding expression to be ignored and not run at all. Used as a lower-level binding form, it will cause the binding to be ignored or not created.
?! Any symbol beginning with ?! works similarly to the & form. It is declared ignored or not evaluated at all.
? Any symbol beginning with ? but not ?! will make a binding of the symbol obtained by removing the ?, and will make an ignorable declaration for this variable.

User-Defined Binders

B* expands to multiple nestings of another macro, PATBIND, analogously to how LET* expands to multiple nestings of LET.

New b* binders may be created by defining a macro named PATBIND-<name>. We discuss the detailed interface of user-defined binders below. But first, note that def-patbind-macro provides a simple way to define certain user binders. For example, this form is used to define the binder for CONS:

(def-patbind-macro cons (car cdr))

This defines a binder macro, patbind-cons, which enables (cons a b) to be used as a binder form. This binder form must take two arguments since two destructor functions, (car cdr), are given to def-patbind-macro. The destructor functions are each applied to the form to produce the bindings for the corresponding arguments of the binder.

There are many cases in which def-patbind-macro is not powerful enough. For example, a binder produced by def-patbind-macro may only take a fixed number of arguments. More flexible operations may be defined by hand-defining the binder macro using the form def-b*-binder.

A binder macro, patbind-<name> must take three arguments: args, forms, and rest-expr. The form

(b* (((foo arg1 arg2) binding1 binding2))
  expr)

translates to a macro call

(patbind-foo (arg1 arg2) (binding1 binding2) expr)

That is:

  • args is the list of arguments given to the binder form,
  • bindings is the list of expressions bound to them, and
  • expr is the result expression to be run once the bindings are in place.

The definition of the patbind-foo macro determines how this gets further expanded. Some informative examples of these binder macros may be found in std/util/bstar.lisp; simply search for uses of def-b*-binder.

Here are some further notes on defining binder macros.

Interaction with packages: The macro patbind-foo is derived from a use of the foo binder using intern-in-package-of-symbol with foo as the package witness. Practially speaking, this means that you can use a binder from a different package (without any package prefix) if either the binder name foo or the macro patbind-foo is imported into your package. Additionally, if foo was defined as a binder in the bar package and not imported into your current package, it can still be invoked as bar::foo. Note also that when defining a binder using def-b*-binder, the args, forms, and rest-expr formals are always in the ACL2 package.

Often the simplest way to accomplish the intended effect of a patbind macro is to have it construct another b* form to be recursively expanded, or to call other patbind macros. See, for example, the definition of patbind-list.

Patbind macros for forms that are truly creating bindings should indeed use b* (or patbind, which is what b* expands to) to create these bindings, so that ignores and nestings are dealt with uniformly. See, for example, the definition of patbind-nths.

In order to get good performance, destructuring binders such as are produced by def-patbind-macro bind a variable to any binding that isn't already a variable or quoted constant. This is important so that in the following form, (foo x y) is run only once:

(b* (((cons a b) (foo x y))) ...)

In these cases, it is good discipline to check the new variables introduced using the macro check-vars-not-free; since ACL2 does not have gensym, this is the best option we have. See any definition produced by def-patbind-macro for examples, and additionally patbind-nths, patbind-er, and so forth.

Subtopics

B*-binders
List of the available directives usable in b*
Def-b*-binder
Introduce a new form usable inside b*.