• Top
    • Documentation
    • Books
    • Recursion-and-induction
    • Boolean-reasoning
    • Projects
    • Debugging
    • Std
      • Std/lists
      • Std/alists
      • Obags
      • Std/util
        • Defprojection
        • Deflist
        • Defaggregate
          • Tag
          • Prod-cons
          • Defaggrify-defrec
        • Define
        • Defmapping
        • Defenum
        • Add-io-pairs
        • Defalist
        • Defmapappend
        • Defarbrec
        • Returns-specifiers
        • Define-sk
        • Defmax-nat
        • Defines
        • Error-value-tuples
        • Defmin-int
        • Deftutorial
        • Extended-formals
        • Defrule
        • Defval
        • Defsurj
        • Defiso
        • Defconstrained-recognizer
        • Deffixer
        • Defmvtypes
        • Defconsts
        • Support
        • Defthm-signed-byte-p
        • Defthm-unsigned-byte-p
        • Std/util-extensions
        • Defthm-natp
        • Defund-sk
        • Defmacro+
        • Defsum
        • Defthm-commutative
        • Definj
        • Defirrelevant
        • Defredundant
      • Std/strings
      • Std/io
      • Std/osets
      • Std/system
      • Std/basic
      • Std/typed-lists
      • Std/bitsets
      • Std/testing
      • Std/typed-alists
      • Std/stobjs
      • Std-extensions
    • Proof-automation
    • Macro-libraries
    • ACL2
    • Interfacing-tools
    • Hardware-verification
    • Software-verification
    • Testing-utilities
    • Math
  • Std/util

Defaggregate

Introduce a record structure, like a struct in C.

Introduction

Defaggregate introduces a recognizer, constructor, and accessors for a new record-like structure. It is similar to struct in C or defstruct in Lisp.

Basic example:

(defaggregate employee     ;; structure name
  (name salary position)   ;; fields
  :tag :employee           ;; options
  )

This example would produce:

  • A recognizer, (employee-p x),
  • A constructor, (employee name salary position),
  • An accessor for each field, e.g., (employee->name x),
  • An extension of b* to easily destructure these aggregates,
  • Macros for making and changing aggregates,
    • (make-employee :name ... :salary ...)
    • (change-employee x :salary ...)
  • Basic theorems relating these new functions.

General form:

(defaggregate name
  Fields
  [Option]*              ;; i.e., :keyword value
  [/// other-events])    ;; optional, starts with the symbol ///

The name acts like a prefix; the function and theorem names we generate will be based on this name.

The Fields describe what fields each instance of the structure will have. The example above shows only the very simplest syntax, but fields can also have well-formedness requirements, documentation, etc.; see below.

The Options control various settings on the structure, and many options are described below. Options can actually come before or after the fields (or both).

The other-events is just a place for arbitrary other events to be put, as in define. This is mainly intended as a book structuring device, to allow you to keep related theorems near your aggregate.

Structure Tags

The :tag of every aggregate must either be:

  • A keyword symbol that typically shares its name with the name of the aggregate, e.g., in the "employee" aggregate the tag is :employee; or
  • nil, to indicate that you want a tagless aggregate.

How are tags used? Each instance of a tagged aggregate will be a cons tree whose car is the tag. This requires some overhead—one cons for every instance of the aggregate—but allows us to compare tags to differentiate between different kinds of aggregates. A tagless aggregate avoids this overhead, but you give up the ability to easily distinguish different kinds of tagless aggregates from one another.

To avoid introducing many theorems about car, we use an alias named tag. Each tagged defaggregate results in three tag-related theorems:

  1. Tag of constructor:
    (defthm tag-of-example
      (equal (tag (example field1 ... fieldN))
             :example))
  2. Tag when recognized:
    (defthm tag-when-example-p
      (implies (example-p x)
               (equal (tag x) :example))
      :rule-classes ((:rewrite :backchain-limit-lst 0)
                     (:forward-chaining)))
  3. Not recognized when tag is wrong:
    (defthm example-p-when-wrong-tag
      (implies (not (equal (tag x) :example))
               (equal (example-p x)
                      nil))
      :rule-classes ((:rewrite :backchain-limit-lst 1)))

These theorems seem to perform well and settle most questions regarding the disjointness of different kinds of aggregates. In case the latter rules become expensive, we always add them to the tag-reasoning ruleset, so you can disable this ruleset to turn off almost all tag-related reasoning.

Syntax of Fields

To describe the aggregate's fields, you can make use of extended-formals syntax. This syntax means that fields can be optional and use keyword/value options. One can also use this syntax to describe a particular field of an aggregate -- by providing documentation or specifying a predicate that the field must satisfy. Here is an example:

(defaggregate employee

  ((name   "Should be in Lastname, Firstname format"
           stringp :rule-classes :type-prescription)

   (salary "Annual salary in dollars, nil for hourly employees"
           (or (not salary) (natp salary))
           :rule-classes :type-prescription)

   (rank "Employee rank.  Can be empty."
         (implies rank (and (characterp rank)
                            (alpha-char-p rank))))

   (position (and (position-p position)
                  (salary-in-range-for-position-p salary position))
             :default :peon))

  :tag :employee)

The "guard" for each field plays three roles:

  • It is a guard on the constructor
  • It is a well-formedness requirement enforced by the recognizer
  • It is a theorem about the return type of the accessor.

The return-type theorem requires some special attention. By default, the return-type theorem is an ordinary ACL2::rewrite rule. When this is not appropriate, e.g., for name above, you may wish to use a different :rule-classes option.

The embedded xdoc documentation gets incorporated into the automatically-created documentation for the aggregate in a sensible way. This is overridden by the :suppress-xdoc t option.

The :default value only affects the Make macro (see below).

Options

Layout

By default, aggregates are represented with :layout :alist, but you can also choose other layouts.

The :alist format provides the best readability/debuggability but is the worst layout for execution/memory efficiency. This layout represents instances of your structure using an alist-like format where the name of each field is next to its value. When printing such an object you can easily see the fields and their values, but creating these objects requires additional consing to put the field names on, etc.

The :tree or :fulltree layouts provides the best efficiency and worst readability. They pack the fields into a compact tree structure, without their names. In :tree mode, any (nil . nil) pairs are compressed into just nil. In :fulltree mode this compression doesn't happen, which might marginally save time if you know your fields will never be in pairs of nils. Tree-based structures require minimal consing, and each accessor simply follows some minimal, fixed car/cdr path into the object. The objects print as horrible blobs of conses that can be hard to inspect.

The :list layout strikes a middle ground, with the fields of the object laid out as a plain list. Accessing the fields of such a structure may require more cdr operations than for a :tree layout, but at least when you print them it is still pretty easy to tell what the fields are.

Honsed Aggregates

By default, :hons is nil and the constructor for an aggregate will build the object using ordinary conses. However, when :hons is set to t, we instead always use hons when building these aggregates.

Honsing is only appropriate for some structures. It is a bit slower than consing, and should typically not be used for aggregates that will be constructed and used in an ephemeral manner. If you are going to hons your structures, you should probably use a :tree or :fulltree layout.

Other Options

:pred
Name of the recognizer for the aggregate -- must be a valid symbol for a new function. Defaults to agg-p, where agg is the name of the aggregate.
:mode
Mode for the introduced functions -- must be either :program or :logic. Defaults to the current ACL2::defun-mode.
:already-definedp
Advanced option that may be useful for mutually-recursive recognizers. This means: generate all ordinary defaggregate functions and theorems except for the recognizer. For this to work, you have to have already defined a "compatible" recognizer.
:parents, :short, :long
These options are as in xdoc. Whatever you supply for :long will follow some automatically generated documentation that describes the fields of the aggregate. If you don't want that xdoc, you can turn it off with the next option.
:suppress-xdoc
You can use :suppress-xdoc t to prevent xdoc from being created for this aggregate. Overrides the automatically generated documentation as well as:parents, :short, and :long.
:extra-field-keywords
Advanced option for people who are writing extensions of defaggregate. This tells defaggregate to tolerate (and ignore) certain additional keywords in its fields. The very advanced user can then inspect these fields after submitting the aggregate, and perhaps use them to generate additional events.
:verbosep
You can use :verbosep t to turn off output hiding. This option is generally meant for debugging failures in defaggregate.
:rest
This option is deprecated. Please use the new /// syntax, instead.

Dependent Requirements

The embedded "guard" in each extended formal allows you to naturally express simple requirements, e.g., arity should be a natural and args should be an true-listp. But what if you need something more like dependent types, e.g., say that arity is supposed to always match the length of args?

It's valid to refer to other fields within the guards of an embedded formal, so one way we could write this would be, e.g.,:

(defaggregate mytype
   ((arity natp :rule-classes :type-prescription)
    (args  (and (true-listp args)
                (equal (len args) arity)))))

This is perfectly valid, but you may sometimes prefer not to embed these dependent requirements directly in the fields. For instance, in the example above, the result-type theorem about args becomes two ACL2::rewrite rules. It would probably be better for the true-listp part to be a ACL2::type-prescription rule. But the len requirement doesn't make sense as a :type-prescription.

To work around this, you could use an explicit, compound :rule-classes form with separate :corollary theorems. This gets very ugly, because you have to write out each corollary in full, e.g.,:

(defaggregate mytype
  ((arity natp :rule-classes :type-prescription)
   (args  (and (true-listp args)
               (equal (len args) arity))
          :rule-classes
          ((:type-prescription :corollary
                               (implies (force (mytype-p x))
                                        (true-listp (mytype->args x))))
           (:rewrite :corollary
                     (implies (force (mytype-p x))
                              (equal (len (mytype->args x))
                                     (mytype->arity x))))))))

So you may instead prefer to use the alternate :require syntax. In this case, we would have:

(defaggregate mytype
  ((arity natp :rule-classes :type-prescription)
   (args  true-listp :rule-classes :type-prescription))
  :require
  ((len-of-mytype->args (equal (len args) arity))))

This would result in an ordinary ACL2::type-prescription return-type theorems for both arity and args, and a separate rewrite rule to deal with the length dependency:

(defthm len-of-mytype->args
  (implies (force (mytype-p x))
           (equal (len (mytype->args x))
                  (mytype->arity x))))

The general form of each :require form is:

(theorem-name conclusion [:rule-classes ...])

Where conclusion may mention any of the fields of the aggregate. Each requirement becomes a guard for the constructor, a well-formedness requirement in the recognizer, and a theorem about the accessors of your structure, exactly like the simple requirements on each field.

Make and Change Macros

Direct use of the constructor is discouraged. Instead, we introduce two macros with every aggregate. The make macro constructs a fresh aggregate when given values for its fields:

(make-example :field1 val1 :field2 val2 ...)
   -->
(example val1 val2 ...)

The change macro is similar, but is given an existing object as a starting point. It may be thought of as:

(change-example x :field2 val2)
   -->
(make-example :field1 (example->field1 x)
              :field2 val2
              :field3 (example->field3 x)
              ...)

There are some strong advantages to using these macros instead of calling the constructor directly.

  • The person writing the code does not need to remember the order of the fields
  • The person reading the code can see what values are being given to which fields.
  • Fields whose value should be nil (or some other :default) may be omitted from the make macro.
  • Fields whose value should be left alone can be omitted from the change macro.

These features make it easier to add new fields to the aggregate later on, or to rearrange fields, etc.

Integration with b*

Defaggregate automatically introduces a pattern binder that integrates into b*. This provides a concise syntax for destructuring aggregates. For instance:

(b* ((bonus-percent 1/10)
     ((employee x)  (find-employee name db))
     (bonus         (+ (* x.salary bonus-percent)
                       (if (equal x.position :sysadmin)
                           ;; early christmas for me, har har...
                           (* x.salary 2)
                         0))))
  bonus)

Can loosely be thought of as:

(b* ((bonus-percent 1/10)
     (temp          (find-employee name db))
     (x.name        (employee->name temp))
     (x.salary      (employee->salary temp))
     (x.position    (employee->position temp))
     (bonus         (+ (* x.salary bonus-percent)
                       (if (equal x.position :sysadmin)
                           ;; early christmas for me, har har...
                           (* x.salary 2)
                         0))))
  bonus)

For greater efficiency in the resulting code, we avoid binding components which do not appear to be used, e.g., we will not actually bind x.name above.

Detecting whether a variable is needed at macro-expansion time is inherently broken because we can't truly distinguish between function names, macro names, variable names, and so forth. It is possible to trick the binder into including extra, unneeded variables, or into optimizing away bindings that are necessary. In such cases, the ACL2 user will be presented with either "unused variable" or "unbound variable" error. If you can come up with a non-contrived example where this is really a problem, we might consider developing some workaround, perhaps extended syntax that lets you suppress the optimization altogether.

Extra Binder Names

You can instruct the b* binder to understand additional, "derived" fields for certain structures.

Example. Suppose we are dealing with student structures that have separate firstname and lastname fields. We might find that we often want to use the student's full name. We can explain to the b* binder that we want it to understand the syntax x.fullname by giving the :extra-binder-names argument.

(defaggregate student
  ((firstname stringp)
   (lastname  stringp)
   (grade     natp))
 :extra-binder-names (fullname))

When we do this, the b* binder will look for occurrences of x.fullname and, if any are found, it will bind them to (student->fullname x). For this to be work at all, we have to define this function ourselves, e.g.,:

(define student->fullname ((x student-p))
  :returns (fullname stringp :rule-classes :type-prescription)
  (str::cat (student->firstname x)
            " "
            (student->lastname x)))

Once we do this, we can freely write x.fullname wherever we previously would have had to call (student->fullname x). For instance:

(b* ((fred (make-student :firstname "Fredrick"
                         :lastname "Flintstone"
                         :grade 7))
    ((student fred)))
  (str::cat "Fred's full name is " fred.fullname "."))

Nicely produces "Fred's full name is Fredrick Flintstone".

Subtopics

Tag
Get the tag from a tagged object.
Prod-cons
Special constructor for products that collapses (nil . nil) into nil.
Defaggrify-defrec
Add defaggregate-style emulation for defrec records.