Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The HOL Logic in ML

In this chapter, the concrete representation of the HOL logic is described. This involves describing the ML functions that comprise the interface to the logic (up to and including §Theorems); the quotation, parsing, and printing of logical types and terms §Quotations; the representation of theorems (§Theorems); the representation of theories (§Theories); the fundamental HOL theory bool (§bool); the primitive rules of inference (§Rules); and the methods for extending theories (throughout §Theories and also later in §bossLib). It is assumed that the reader is familiar with ML. If not, the introduction to ML in Getting Started with HOL in the TUTORIAL Part should be read first.

The HOL system provides the ML types hol_type and term which implement the types and terms of the HOL logic, as defined in §LOGIC. It also provides primitive ML functions for creating and manipulating values of these types. Upon this basis the HOL logic is implemented. The key idea of the HOL system, due to Robin Milner, and discussed in this chapter, is that theorems are represented as an abstract ML type whose only pre-defined values are axioms, and whose only operations are rules of inference. This means that the only way to construct theorems in HOL is to apply rules of inference to axioms or existing theorems; hence the consistency of the logic is preserved.

The purpose of the meta-language ML is to provide a programming environment in which to build theorem proving tools to assist in the construction of proofs. When the HOL system is built, a range of useful theorems is pre-proved and a set of tools pre-defined. The basic system thus offers a rich initial environment; users can further enrich it by implementing their own application specific tools and building their own application specific theories.

Types

The allowed types depend on which type constants have been declared in the current theory. See §Theories for details of how such declarations are made. There are two primitive constructor functions for values of type hol_type

    mk_vartype : string -> hol_type
    mk_thy_type :
      {Args: hol_type list, Thy: string, Tyop: string} -> hol_type

The function mk_vartype constructs a type variable with a given name; it gives a warning if the name is not an allowable type variable name (i.e., not an apostrophe (') followed by an alphanumeric). The function mk_thy_type constructs a compound type from a record {Tyop,Thy,Args} where Tyop is a string representing the name of the type operator, Thy is a string representing the theory that Tyop was declared in, and Args is a list of types representing the arguments to the operator. Function types $\sigma_1\to\sigma_2$ of the logic are represented in ML as though they were compound types $(\sigma_1,\sigma_2)$fun (in §LOGIC, however, function types were not regarded as compound types).

The evaluation of $$ \mathtt{mk\_thy\_type}\{\mathtt{Tyop} = \mathit{name},\, \mathtt{Thy} = \mathit{thyname},\, \mathtt{Args} = [\sigma_1, \cdots ,\sigma_n]\} $$ fails if

  1. $\mathit{name}$ is not a type operator of theory $\mathit{thyname}$;
  2. $\mathit{name}$ is a type operator of theory $\mathit{thyname}$, but its arity is not $n$;
  3. $\mathit{thyname}$ is not the name of a theory in the current context.

For example, mk_thy_type{Tyop="bool", Thy="min", Args=[]} evaluates to an ML value of type hol_type representing the type bool.

Type constants may be bound to ML values and need not be repeatedly constructed: e.g., the type built by mk_thy_type{Tyop="bool", Thy="min", Args=[]} is abbreviated by the ML value bool. Similarly, function types may be constructed with the infix ML function -->. A few common type variables have been constructed and bound to ML identifers, e.g., alpha is the type variable 'a and beta is the type variable 'b. Thus the ML code alpha --> bool is equal to, but much more concise than

> mk_thy_type{Tyop="fun", Thy="min",
              Args=[mk_vartype "'a",
                    mk_thy_type{Tyop="bool", Thy="min", Args=[]}]}
val it = “:α -> bool”: hol_type

There are two primitive destructor functions for values of type hol_type:

    dest_vartype : hol_type -> string
    dest_thy_type :
      hol_type -> {Args: hol_type list, Thy: string, Tyop: string}

The function dest_vartype extracts the name of a type variable. A compound type is destructed by the function dest_thy_type into the name of the type operator, the name of the theory it was declared in, and a list of the argument types; dest_vartype and dest_thy_type are thus the inverses of mk_vartype and mk_thy_type, respectively. The destructors fail on arguments of the wrong form.

Terms

The four primitive kinds of terms of the logic are described in §LOGIC. The ML functions for manipulating these are described in this section. There are also derived terms that are described in §Derived Syntactic Forms.

At any time, the terms that may be constructed depends on which constants have been declared in the current theory. See §Theories for details of how such declarations are made.

There are four primitive constructor functions for values of type term:

    mk_var : string * hol_type -> term

mk_var($x$,$\sigma$) evaluates to a variable with name $x$ and type $\sigma$; it always succeeds.

    mk_thy_const :
      {Name: string, Thy: string, Ty: hol_type} -> term

mk_thy_const{Name =$\;\mathit{c}$,Thy =$\;\mathit{thyname}$,Ty =$\;\sigma$} evaluates to a term representing the constant with name $c$ and type $\sigma$; it fails if:

  1. $c$ is not the name of a constant in the theory $\mathit{thyname}$;
  2. $\sigma$ is not an instance of the generic type of $c$ (the generic type of a constant is established when the constant is defined; see §Theories).
    mk_comb : term * term -> term

mk_comb($t_1$,$t_2$) evaluates to a term representing the combination $t_1\ t_2$. It fails if:

  1. the type of $t_1$ does not have the form $\sigma_1\to\sigma_2$;
  2. the type of $t_1$ has the form $\sigma_1\to\sigma_2$, but the type of $t_2$ is not equal to $\sigma_1$.
    mk_abs : term * term -> term

mk_abs($x$,$t$) evaluates to a term representing the abstraction $\lambda x.\;t$; it fails if $x$ is not a variable.

There are four primitive destructor functions on terms:

    dest_var : term -> string * hol_type
    dest_thy_const :
      term -> {Name: string, Thy: string, Ty: hol_type}
    dest_comb : term -> term * term
    dest_abs : term -> term * term

These are the inverses of mk_var, mk_thy_const, mk_comb and mk_abs, respectively. They fail when applied to terms of the wrong form. Other useful destructor functions are rator, rand, bvar, body, lhs and rhs. See §REFERENCE for details.

The function

    type_of : term -> hol_type

returns the type of a term.

The function

    aconv : term -> term -> bool

implements the $\alpha$-convertibility test for $\lambda$-calculus terms. From the point of view of the HOL logic, $\alpha$-convertible terms are identical. A variety of other functions are available for performing $\beta$-reduction (beta_conv), $\eta$-reduction (eta_conv), substitution (subst), type instantiation (inst), computation of free variables (free_vars) and other common term operations. See §REFERENCE for more details.

Quotations, Parsing and Printing

It would be tedious to always have to input types and terms using the constructor functions. The HOL system, adapting the approach taken in LCF, has special quotation parsers for HOL types and terms which enable types and terms to be input using a fairly standard syntax. For example, the ML expression “:bool -> bool” denotes exactly the same value (of ML type hol_type) as

> mk_thy_type{
   Tyop = "fun",Thy = "min",
   Args = [
     mk_thy_type{Tyop = "bool", Thy = "min", Args = []},
     mk_thy_type{Tyop = "bool", Thy = "min", Args = []}
   ]}
val it = “:bool -> bool”: hol_type

and the expression “\x. x + 1” can be used instead of

> val numty = mk_thy_type{Tyop="num",Thy="num",Args=[]}
val numty = “:num”: hol_type
> val tedious = mk_abs
   (mk_var("x",numty),
    mk_comb(mk_comb (
      mk_thy_const {
        Name="+",Thy="arithmetic",Ty=numty --> numty --> numty
      },
      mk_var("x", numty)
      ),
      mk_comb(
        mk_thy_const{
          Name="NUMERAL", Thy="arithmetic", Ty=numty-->numty
        },
        mk_comb(
          mk_thy_const{
            Name="BIT1", Thy="arithmetic", Ty=numty-->numty
          },
          mk_thy_const{
            Name="ZERO", Thy="arithmetic", Ty=numty
          }))));
val tedious = “λx. x + 1”: term
> val parsed = ``\x. x + 1``;
val parsed = “λx. x + 1”: term
> aconv tedious parsed;
val it = true: bool

The HOL printer, which is integrated into the ML toplevel loop, also outputs types and terms using this syntax. Types are printed in the form “:$\mathit{type}$. For example, the ML value of type hol_type representing $\alpha\to(\mathtt{ind}\to\mathtt{bool})$ would be printed out as below:

> let
    val ind_ty = mk_thy_type{Tyop="ind", Thy="min", Args=[]}
  in
    alpha --> (ind --> bool)
  end;
val it = “:α -> ind -> bool”: hol_type

Similarly, terms are printed in the form $\mathit{term}$, as in the session above printing the term $\lambda$x. x + 1”. The leading colon is used to distinguish a type quotation from a term quotation: the former have the form “:$\cdots$ and the latter have the form $\cdots$.

Lexical Matters

The name of a HOL variable can be any ML string, but the quotation mechanism will parse only names that are identifiers (see §Identifiers below). Using non-identifiers as variable names is discouraged except in special circumstances (for example, when writing derived rules that generate variables with names that are guaranteed to be different from existing names). The name of a type variable in the HOL logic is formed by a prime/apostophe (') followed by an alphanumeric which itself contains no prime (see §Type Variable Names for examples). The name of a type constant or a term constant in the HOL logic can be any identifier, although some names are treated specially by the HOL parser and printer and should therefore be avoided.

Identifiers

In addition to special forms already present in the relevant grammar, a HOL identifier can be of two forms:

  1. A finite sequence of alphanumerics starting with a letter. The underscore character is considered a digit character, and so can occur after an identifier's first letter. Greek characters (roughly Unicode range U+0370 to U+03FF) are also letters, except for $\lambda$~(U+03BB), which is treated as a symbol. HOL is case-sensitive: upper and lower case letters are considered to be different.

    Digits are the ASCII characters 0–9, the underscore character, and the Unicode subscripts and superscripts. The apostrophe character is special: it is not a letter, but can appear as part of an alphanumeric term identifier after the first letter. It must appear at the start of a type variable's name, and can also appear in the term context as a sequence of apostrophes on their own.

  2. A symbolic identifier, i.e., a finite sequence formed by any combination of the ASCII symbols and the Unicode symbols. The basic ASCII symbols are

      #  ?  +  *  /  \  =  <  >  &  %  @  !  :  |  -  ^  `
    

    Use of the caret and back-tick characters is complicated by the fact that these characters have special meaning in the quotation mechanism; see §Quotations and Antiquotations. The dollar-sign ($) can also be used to form symbolic identifiers, but only in tokens where it is the only symbol. Thus, $, $$, and $$$ will all lex as identifiers.

    This restriction arises because of the other uses to which the dollar sign is put:

    • The dollar can be used as an escaping mechanism to remove special syntactic treatment of other identifiers. Thus, $+ and $if are effectively special forms of the tokens + and if respectively.
    • Finally, the dollar can also be used as a namespace separator character, giving unambiguous “long form” identifiers. For example, the token bool$COND is an unambiguous way of writing the COND constant from theory segment bool.

    The ASCII grouping symbols (braces, brackets, and parentheses), and the tilde (~), full-stop (.), comma (,), semi-colon (;) and hyphen (-) characters are called non-aggregating characters. Unless the desired token is already present in the grammar, these characters do not combine with themselves or other symbolic characters. Thus, the string "((" is viewed as two tokens, as are +; and "-+".

    Unicode code characters that are not letters or digits are regarded as symbolic. The non-aggregating Unicode characters are listed in the table below:

CharacterCodepointCharacterCodepoint
$\neg$U+00AC$\langle$U+27E8
$\lceil$U+2308$\rangle$U+27E9
$\rceil$U+2309U+2983
$\lfloor$U+230AU+2984
$\rfloor$U+230BU+2987
$\llbracket$U+27E6U+2988
$\rrbracket$U+27E7U+2772
U+2773

Table: Non-aggregating Unicode characters

  1. A number is a string of one or more digits. If not the initial digit, an underscore can be used within the sequence to provide spacing. In order to distinguish different types of numbers a single character suffix may be used: for example 3n is a natural number while 3i is an integer. The 0x and 0b prefixes may also be used to change the base of the number. If the 0x prefix is used, hexadecimal ‘digits’ af and AF can also be used. See also §Numerals.
Separators

The separators used by the HOL lexical analyser are (with ASCII codes in brackets):

space (32), carriage return (13), line feed (10), tab (^I, 9), form feed (^L, 12)

Special identifiers

The following valid identifiers are used by the grammar in the theory of booleans, and thus in all descendant theories as well. They should not be used as the name of a variable or a constant unless the user is very confident of their ability to mess with grammars.

   let  in  and  \  .  ;  =>  |  :  :=  with  updated_by  case  of
Type variable names

The name of a type variable in the HOL logic is a string beginning with a prime (') followed by an alphanumeric which itself contains no prime; for example all of the following are valid type variable names except for the last:

   'a   'b   'cat   'A11   'g_a_p   'f'oo
User tokens

In general, a HOL user has a great deal of freedom to create their own syntax, involving special tokens quite apart from variables and names for constants. For example, the if-then-else syntax for the conditional operator has special tokens (the “if”, “then” and “else”) that are not names for variables, nor constants (the underlying constant is actually called COND). In order to make sure that the operations of printing and parsing tokens are suitably inverse to each other, users should not create tokens that include whitespace, or the comment strings ((* and *)).

Literals

There are two classes of literal in HOL's term syntax: numbers and strings (which latter also includes a treatment of character literals). String literals are a convenient way to write large terms of type :char list; numerals are a convenient way to write values of type :num. In addition, both string and numeric literals can be injected into other types, so that, for example, it is possible to write 23 and have the system see it as a rational number. For more on these syntaxes, see §Numerals and §Strings.

Unicode vs ASCII

As the definition of parsed in the session above suggests, there are ASCII alternatives to the Unicode syntax. To repeat that definition:

> val parsed = ``\x. x + 1``;
val parsed = “λx. x + 1”: term

The backslash can be used instead of $\lambda$, and the `` symbol can be used instead of both the “ and ” symbols. Similarly, the logical connectives have both ASCII and Unicode forms (see §Grammar below), and either can be used (even within the same term), except that the begin- and end-delimiters must be of the same type. Usually the system prefers to print its output with the Unicode symbols. Thus:

> val t1 = ``!x y. x < y ==> ?z. x + z = y``;
val t1 = “∀x y. x < y ⇒ ∃z. x + z = y”: term

> val t2 = “∀x. ∃y. x < y ∧ y < x + 1”;
val t2 = “∀x. ∃y. x < y ∧ y < x + 1”: term

> val t3 = ``!x. ∃y. x < y /\ y < x + 1``;
val t3 = “∀x. ∃y. x < y ∧ y < x + 1”: term

§Parsing and Printing has more detailed information about the capabilities of the term and type parsing and printing facilities in the system. The remainder of this section provides a brief overview of what is possible.

Type Inference

Notice that there is no explicit type information in \x.x+1. The HOL type checker knows that 1 has type :num and + has type :num->(num->num). From this information it can infer that both occurrences of x in \x. x+1 could have type :num. This is not the only possible type assignment; for example, the first occurrence of x could have type bool and the second one have type :num. In that case there would be two different variables with name x, namely x$_{\mathtt{bool}}$ and x$_{\mathtt{num}}$, the second of which is free. However, the only way to construct a term with this second type assignment is by using the ML API, since the type checker uses the heuristic that all variables in a term with the same name have the same type. This is illustrated in the following session.

> ``x = (x = 1)``;
Exception- HOL_ERR
  (at Preterm.type-analysis: on line 1, characters 7-11:
       
Type error in function application.
  Function: $= x :num -> bool
  Argument: x = 1 :bool
  Reason: Attempt to unify different type operators: num$num a...
) raised

The desired value can be directly constructed by the primitive constructor functions:

> mk_eq
  (mk_var("x",bool),
   mk_eq(mk_var("x",numty),
         numSyntax.mk_numeral (Arbnum.fromString "1")));
val it = “x ⇔ x = 1”: term

The original quotation type checker was designed and implemented by Robin Milner. It employs heuristics like the one above to infer a sensible type for all variables occurring in a term.

At times, the user may want to control the exact type of a subterm. To support such functionality, types can be explicitly indicated by following any subterm with a colon and then a type. For example, “f(x:num):bool” will type check with f and x getting types :num->bool and :num respectively. This treatment of types within quotations is inherited from LCF.

Parentheses and Precedence

As with programming languages, the grammar governing the parsing of terms and types includes a notion of precedence. As with standard mathematics, for example, if we write 2 + 3 * 6, we expect this to denote the abstract syntax tree that groups the 3 and 6 together, giving a value of 20 for the term. Again as is usual, to adjust parses in the face of precedence one can use parentheses:

> EVAL “2 + 3 * 6”;
val it = ⊢ 2 + 3 * 6 = 20: thm

> EVAL “(2 + 3) * 6”;
val it = ⊢ (2 + 3) * 6 = 30: thm

Function application can be seen as an invisible high-precedence (or “tightly binding”) infix operator so that f x + 6 is an addition term, with the application of f to x being added to 6. This makes HOL syntax more like functional programming: one typically doesn't bother to write f(x) because f x suffices. Of course, sometimes arguments do need parentheses. For example, f(x + 6).

Finally, drawing inspiration from the Haskell programming language, HOL also supports the dollar-sign as a low precedence function application symbol. In this way, one has an option that can result in needing to write fewer parentheses:

> EVAL “FACT $ SUC $ 2 + 3”;
val it = ⊢ FACT (SUC (2 + 3)) = 720: thm

Note that, as above, the pretty-printer will always print terms with the “invisible” function application symbol and parentheses as necessary, even if they were input with dollar-signs.

Viewing the Grammar

The behaviour of the HOL quotation parser and printer is determined by the current grammar. Thus, a familiarity with the basic vocabulary of the standard collection of HOL theories is important if one is to use HOL effectively. One can examine the current grammar used by the parser with the functions type_grammar and term_grammar.

For example, in the following session, we see that the type grammar used in the startup context of HOL has the type operators fun, sum, prod, list, recspace, num, option, one, cv, ind, and bool:

> type_grammar();
val it =
   Rules:
     (50)   TY  ::=  TY -> TY [fun] (R-associative)
     (60)   TY  ::=  TY + TY [sum] (R-associative)
     (70)   TY  ::=  TY # TY [prod] (R-associative)
            TY  ::=  bool | cv | (TY, TY) fun | ind | TY itself |
                     TY list | num | one | TY option |
                     (TY, TY) prod | TY recspace | TY set |
                     (TY, TY) sum | unit
            TY  ::=  TY[TY] (array type)
   Type abbreviations:
     bool             = min$bool              
     cv               = cv$cv                 
     (α, β) fun       = (α, β) min$fun        
     ind              = min$ind               
     α itself         = α bool$itself         
     α list           = α list$list           
     num              = num$num               
     one              = one$one                [not printed]
     α option         = α option$option       
     (α, β) prod      = (α, β) pair$prod      
     α recspace       = α ind_type$recspace   
     α set            = (α, min$bool) min$fun  [not printed]
     (α, β) sum       = (α, β) sum$sum        
     unit             = one$one               :
   type_grammar.grammar

Also, fun, sum, and prod have infix notation (->), (+), and (#), respectively, with different binding strengths: # (with 70) binds stronger than + (60), which binds stronger than -> (50). All postfix type operators bind more strongly than the infixes.

The next session shows the (abbreviated) output from invoking the term_grammar function in the startup HOL environment. The deleted output includes more rules, a listing of all constants known to the system, including prefix operators, and a list of all overloadings currrently in force. The grammar rules shown include precedence levels, concrete syntax, and how various forms map to actual names (e.g., if-then-else maps to the name COND, and maps to actual name ==>).

> term_grammar();
[...Lines elided...]
                | TM "|->" TM  [  combinpp.leftarrow]
                | TM "⇎" TM  [<=/=>] | TM "<=/=>" TM  
                | TM "⇔" TM  [<=>] | TM "<=>" TM  
                    (non-associative)
   (200)  TM  ::= TM "⇨ᵣ" TM  [suspendimp] | TM "⇒" TM  [==>]
                | TM "==>" TM  
                    (R-associative)
   (300)  TM  ::= TM "∨" TM  [\/] | TM "\/" TM     (R-associative)
   (310)  TM  ::= TM ":>" TM     (L-associative)
   (320)  TM  ::= TM "=+" TM  [UPDATE]   (non-associative)
   (400)  TM  ::= TM "⅋ᵣ" TM  [resconj] | TM "∧" TM  [/\]
                | TM "/\" TM  
                    (R-associative)
   (425)  TM  ::= TM "refines" TM   | TM "partitions" TM  
                | TM "equiv_on" TM   | TM "∉" TM  [NOTIN]
                | TM "NOTIN" TM   | TM "∈" TM  [IN] | TM "IN" TM  
                    (non-associative)
   (450)  TM  ::= TM "≼" TM  [<<=] | TM "<<=" TM  
                | TM "PERMUTES" TM   | TM "HAS_SIZE" TM  
                | TM "⊂" TM  [PSUBSET] | TM "PSUBSET" TM  
                | TM "⊆" TM  [SUBSET] | TM "SUBSET" TM  
                | TM "≥" TM  [>=] | TM ">=" TM   | TM "≤" TM  [<=]
                | TM "<=" TM   | TM ">" TM   | TM "<" TM  
                | TM "⊆ᵣ" TM  [RSUBSET] | TM "RSUBSET" TM  
                | TM "≠" TM   | TM "<>" TM   | TM "=" TM  
[...Output elided...]

Namespace control

In order to provide convenience, the parser deals with overloading and ambiguity. Overloading of numeric literals is discussed in §Overloading of Arithmetic Operators, although any symbol may be overloaded, not just numerals. At times such flexibility is quite useful; however, it can happen that one wishes to explicitly designate a particular constant. In that case, the notation $\mathit{thy}$$$\mathit{const}$ may be used in the parser to designate the constant $\mathit{const}$ declared in theory $\mathit{thy}$. In the following example, the less-than operator is explicitly specified.

> “prim_rec$< x y”;
val it = “x < y”: term

Note how the < symbol is not treated as an infix by the parser when given in “fully-qualified” form. Syntactically, such tokens are never given special treatment by the parser of HOL's concrete syntax.

Ways to Construct Types and Terms

The table below shows ML expressions for various kinds of type quotations. The expressions in the same row are equivalent.

Kind of typeQuotationML expression
Type variable:'$\mathit{alphanum}$mk_vartype("'$\mathit{alphanum}$")
Type constant:$\mathit{op}$
:$\mathit{thy}$$$\mathit{op}$
mk_type("$\mathit{op}$",[])
mk_thy_type{
  Thy="$\mathit{thy}$",Tyop="$\mathit{op}$", Args=[]}
Function type:$\sigma_1$->$\sigma_2$$\sigma_1$ --> $\sigma_2$
Compound type:($\sigma_1,\dots,\sigma_n$)$\mathit{op}$
:($\sigma_1\dots,\sigma_n$)$\mathit{thy}$$$\mathit{op}$
mk_type("$\mathit{op}$",[$\sigma_1,\dots,\sigma_n$])
mk_thy_type{
  Thy="$\mathit{thy}$",Tyop="$\mathit{op}$",
  Args=[$\sigma_1,\dots,\sigma_n$]}

Table: Building Types via Quotations or ML

Equivalent ways of inputting the four primitive kinds of term are shown in the next table.

Kind of termQuotationML expression
Variable$v$:$\sigma$mk_var("$v$",$\sigma$)
Constant$c$:$\sigma$
$\mathit{thy}$$$c$:$\sigma$
mk_const("$c$",$\sigma$)
mk_thy_const{Thy="$\mathit{thy}$",Name="$c$",Ty=$\sigma$}
Combination$t_1$ $t_2$mk_comb($t_1$, $t_2$)
Abstraction\$x$. $t$mk_abs($x$, $t$)

Table: Building Primitive Terms

The following shows a few of these in action:

> val c = mk_const("CONS",
                   mk_vartype("'a") -->
                   mk_type("list", [mk_vartype("'a")]) -->
                   “:'a list”)
val c = “CONS”: term

> dest_thy_const c;
val it = {Name = "CONS", Thy = "list", Ty = “:α -> α list -> α list”}:
   {Name: string, Thy: string, Ty: hol_type}

In addition to the kinds of terms in the tables above, the parser also supports the following syntactic abbreviations.

Abbreviated termMeaningML expression
$t\;t_1 \cdots t_n$($\cdots$(($t$ $t_1$) $t_2$)$\cdots t_n$)list_mk_comb($t$,[$t_1,\dots,t_n$])
\$x_1\cdots x_n$. $t$\$x_1$.$\cdots$\$x_n$. $t$list_mk_abs([$x_1,\dots,x_n$],$t$)

Table: Syntactic abbreviations

Theorems

In §LOGIC, the notion of deduction was introduced in terms of sequents, where a sequent is a pair whose second component is a formula being asserted (a conclusion), and whose first component is a set of formulas (hypotheses). Based on this was the notion of a deductive system: a set of pairs, whose second component is a sequent, and whose first component is a list of sequents. The concept of a sequent following from a list of sequents via a deductive system was then defined: a sequent follows from a list of sequents if the sequent is the last element of some chain of sequents, each of whose elements is either in the list, or itself follows from the list along with earlier elements of the chain, via the deductive system.

A notation for `follows from' was then introduced. That a sequent $(\{t_1,\dots,t_n\},\ t)$ follows from a set of sequents $\Delta$, via a deductive system 𝒟, is denoted by: $t_1,\dots,t_n\vdash_{𝒟,\Delta} t$. (It was noted that where either 𝒟 or Δ were clear by context, their mention could be omitted; and where the set of hypotheses was empty, its mention could be omitted.)

A sequent that follows from the empty set of sequents via a deductive system is called a \textit{theorem} of that deductive system. That is, a theorem is the last element of a proof (in the sense of §LOGIC) from the empty set of sequents. When a pair $(L,(Γ,t))$ belongs to a deductive system, and the list $L$ is empty, then the sequent $(\Gamma,t)$ is called an axiom. Any pair $(L,(Γ,t))$ belonging to a deductive system is called a primitive inference of the system, with hypotheses1 $L$ and conclusion $(Γ,t)$.

A formula in the abstract is represented concretely in HOL by a term whose HOL type is :bool. Therefore, a term of type :bool is used to represent a member of the set of hypotheses of a sequent; and likewise to represent the conclusion of a sequent. Sets in this context are represented by an implementation of the ML signature HOLset supporting operations such as member and union.

A theorem in the abstract is represented concretely in the HOL system by a value with the ML abstract type thm. The type thm has a destructor function

    dest_thm : thm -> term list * term

which returns a pair consisting of a list of the hypotheses and the conclusion, respectively, of a theorem. The order of assumptions in the list should not be relied on. Using dest_thm, two further destructor functions are derived:

    hyp : thm -> term list
    concl : thm -> term

\noindent for extracting the hypothesis list and the conclusion, respectively, of a theorem. A theorem's hypotheses are also available in the set form with the function

    hypset : thm -> term set

The ML type thm does not have a primitive constructor function. In this way, the ML type system protects the HOL logic from the arbitrary and unrecorded construction of theorems, which would compromise the consistency of the logic. (Functions which return theorems as values, e.g., functions representing primitive inferences, are discussed in §Primitive Rules.)

It was mentioned in §LOGIC that the deductive system of HOL includes four axioms.2 In that manual, the axioms were presented in abstract form. Concretely, axioms are just theorem values that are introduced through the use of the ML function new_axiom (see §Theory Operations below). For example, the axiom BOOL_CASES_AX mentioned in §LOGIC is printed in HOL as follows (where T and F are the HOL logic's constants representing truth and falsity, respectively):

> BOOL_CASES_AX;
val it = ⊢ ∀t. (t ⇔ T) ∨ (t ⇔ F): thm

Note the special print format, with the $\vdash$ notation used to indicate ML type thm status; as well as the absence of HOL quotation marks in the ML context.

The session below illustrates the use of the destructor functions:

> hyp BOOL_CASES_AX;
val it = []: term list

> concl BOOL_CASES_AX;
val it = “∀t. (t ⇔ T) ∨ (t ⇔ F)”: term

> type_of it;
val it = “:bool”: hol_type

In addition to the print conventions mentioned above, the printing of theorems prints hypotheses as periods (i.e., full stops or dots). The flag show_assums allows theorems to be printed with hypotheses shown in full. These points are illustrated with a theorem inferred, for example purposes, from another axiom mentioned in §LOGIC, SELECT_AX.

> val th = UNDISCH (SPEC_ALL SELECT_AX);
val th =  [.] ⊢ P ($@ P): thm

> show_assums := true;
val it = (): unit

> th;
val it =  [P x] ⊢ P ($@ P): thm

Primitive Rules of Inference of the HOL Logic

The primitive rules of inference of the logic were described abstractly in §LOGIC. The descriptions relied on meta-variables $t$, $t_1$, $t_2$, and so on. In the HOL logic, infinite families of primitive inferences are grouped together and thought of as single primitive inference schemes. Each family contains all the concrete instances of one particular inference ‘pattern’. These can be produced, in abstract form, by instantiating the meta-variables in LOGIC’s rules to concrete terms.

In HOL, primitive inference schemes are represented by ML functions that return theorems as values. That is, for particular HOL terms, the ML functions return the instance of the theorem at those terms. The ML functions are part of the ML abstract type thm: although thm has no primitive constructors, it has (eight) operations which return theorems as values: ASSUME, REFL, BETA_CONV, SUBST, ABS, INST_TYPE, DISCH and MP.

The ML functions that implement the primitive inference schemes in the HOL system are described below. The same notation is used here as in §LOGIC: hypotheses above a horizontal line and conclusion beneath. The machine-readable ASCII notation is used for the logical constants.

Assumption Introduction

    ASSUME : term -> thm
    ------------
       t |- t

ASSUME $t$ evaluates to $t$ |- $t$. Failure occurs if $t$ is not of type bool.

Reflexivity

    REFL : term -> thm
    --------------
       |- t = t

REFL $t$ evaluates to |- $t = t$. A call to REFL never fails.

Beta-Conversion

    BETA_CONV : term -> thm
    -----------------------------
       |- (λx. t₁)t₂ = t₁[t₂/x]
  • where $t_1$[$t_2$/$x$] denotes the result of substituting $t_2$ for $x$ in $t_1$, with suitable renaming of variables to prevent free variables in $t_2$ becoming bound after substitution. The substitution $t_1[t_2/x]$ is always defined.

BETA_CONV $(\lambda x.\;t_1)t_2$ evaluates to the theorem |- $(\lambda x.\;t_1)t_2 = t_1[t_2/x]$. Failure occurs if the argument to BETA_CONV is not a $\beta$-redex (i.e., is not of the form $(\lambda x.\;t_1)t_2$).

Substitution

    SUBST : (term,thm)Lib.subst -> term -> thm -> thm
       Γ₁ |- t1 = t1'   ...  Γₙ |- tn = tn'   Γ |- t[t1,...,tn]
    --------------------------------------------------------------
                Γ₁ ∪ ... ∪ Γₙ ∪ Γ |- t[t1',...tn']
  • where $t[t_1,\ldots,t_n]$ denotes a term $t$ with some free occurrences of the terms $t_1$, $\dots$, $t_n$ singled out and $t[t'_1,\ldots,t'_n]$ denotes the result of simultaneously replacing each such occurrences of $t_i$ by $t'_i$ (for $1{\leq}i {\leq} n$), with suitable renaming of variables to prevent free variables in $t_i'$ becoming bound after substitution.

The first argument to SUBST is a list of what are effectively pairs in the widely used (τ₁,τ₂)Lib.subst ML type. Each element in these lists is a record of type {redex:τ₁,residue:τ₂} with the intention that the redex value is to be replaced by the residue value. Such values can be constructed with the infix arrow |->, so that, for example:

> 3 |-> "foo";
val it = {redex = 3, residue = "foo"}: {redex: int, residue: string}

In SUBST, the redex values are variables $x_i$ and the theorems are $\Gamma_i\vdash t_i = t'_i$. The second argument is a template term $t[x_1,\ldots,x_n]$ in which occurrences of the variable $x_i$ (where $1 \leq i\leq n$) are used to mark the places where substitutions with $\Gamma_i \vdash t_i = t'_i$ are to be done. Thus

  SUBST [“x1” |-> (Γ₁ |- t1 = t1'), ..., “xn” |-> (Γₙ |- tn = tn')]
        t[x1,...xn]
        (Γ |- t[t1,...tn])

returns ∪ᵢΓᵢ ∪ Γ|- t[t1',...,tn']. Failure occurs if:

  1. any of the arguments are of the wrong form;
  2. the type of $x_i$ is not equal to the type of $t_i$ for some $1\leq i\leq n$.

The following is a somewhat contrived example of using SUBST:

> val th1 = EVAL “1 + 1”;
val th1 = ⊢ 1 + 1 = 2: thm
> val th2 = EVAL “2 * 3”;
val th2 = ⊢ 2 * 3 = 6: thm

> val template = “x + y + x = 10”;
val template = “x + y + x = 10”: term

> val th = EVAL “(1 + 1) + (2 * 3) + (1 + 1)”
val th = ⊢ 1 + 1 + 2 * 3 + (1 + 1) = 10: thm

> val result = SUBST [“x:num” |-> th1, “y:num” |-> th2] template th
val result = ⊢ 2 + 6 + 2 = 10: thm

Abstraction

    ABS : term -> thm -> thm
          Γ |- t1 = t2
    --------------------------
      Γ |- (λx. t1) = (λx. t2)
  • where x is not free in Γ.

ABS “x” (Γ |- t1 = t2) returns the theorem Γ |- (λx. t1) = (λx. t2). Failure occurs if x is not a variable, or x occurs free in any assumption in Γ.

Type Instantiation

    INST_TYPE : (hol_type, hol_type) Lib.subst -> thm -> thm

                          Γ |- t
    ---------------------------------------------------
      Γ[σ₁,...,σₙ/α₁,...,αₙ] |- t[σ₁,...,σₙ/α₁,...,αₙ]
  • where $t[\sigma_1,\dots,\sigma_n/\alpha_1,\dots,\alpha_n]$ denotes the result of substituting (in parallel) the types $\sigma_1$ to $\sigma_n$ for the type variables $\alpha_1$ to $\alpha_n$ in term $t$. Similarly, $\Gamma[\sigma_1,\dots,\sigma_n/\alpha_1,\dots,\alpha_n]$ denotes the result of performing the same substitution to all of the hypotheses in the set $\Gamma$.

INST_TYPE[α₁ |-> σ₁,...,αₙ |-> σₙ] th returns the result of instantiating each occurrence of αᵢ in the theorem th to σᵢ (for $1 \leq \mathtt{i} \leq n$). Failure occurs if an αᵢ is not a type variable.

For example:

> show_types := true;
val it = (): unit
> INST_TYPE [“:α” |-> “:num”] listTheory.LENGTH;
val it =
   ⊢ LENGTH ([] :num list) = (0 :num) ∧
     ∀(h :num) (t :num list). LENGTH (h::t) = SUC (LENGTH t): thm

Discharging an Assumption

    DISCH : term -> thm -> thm
          Γ |- t₂
    ------------------------
      Γ - {t₁} |- t₁ ⇒ t₂
  • Γ - {t₁} denotes the set obtained by removing t₁ from Γ (note that t₁ need not occur in Γ; in this case Γ - t₁ = Γ).

DISCH t₁ (Γ |- t₂) evaluates to the theorem Γ - {t₁} |- t₁ ⇒ t₂. DISCH fails if the term given as its first argument is not of type :bool.

Modus Ponens

    MP : thm -> thm -> thm
       Γ₁ |- t₁ ⇒ t₂       Γ₂ |- t₁
    ---------------------------------
          Γ₁ ∪ Γ₂ |- t₂

MP takes two theorems (in the order shown above) and returns the result of applying Modus Ponens; it fails if the arguments are not of the right form.

Oracles

HOL extends the LCF tradition by allowing the use of an oracle mechanism, enabling arbitrary formulas to become elements of the thm type. By use of this mechanism, HOL can utilize the results of arbitrary proof procedures. In spite of such liberalness, one can still make strong assertions about the security of ML objects of type thm.

To avoid unsoundness, a tag is attached to any theorem coming from an oracle. This tag is propagated through every inference that the theorem participates in (much as ordinary assumptions are propagated in the inference rule MP). If it happens that falsity becomes derived, the offending oracle can be found by examining the tags component of the theorem. A theorem proved without use of any oracle will have an empty tag, and can thus be considered to have been proved solely by deductive steps in the HOL logic.

A tagged theorem can be created via

    mk_oracle_thm : string -> term list * term -> thm

which directly creates the requested theorem and attaches the given tag to it. The tag is created with a call to

    read : string -> tag

As well as providing principled access to the results of external reasoners, tags are used to implement some useful ‘system’ operations on theorems. For example, one can directly create a theorem via the function mk_thm. The tag MK_THM gets attached to each theorem created with this call. This allows users to directly create useful theorems, e.g., to use as test data for derived rules of inference. Another tag is used to implement so-called ‘validity checking’ for tactics.

The tags in a theorem can be viewed by setting Globals.show_tags to true.

> Globals.show_tags := true;
val it = (): unit

> mk_thm([], Term `F`);
val it = [oracles: MK_THM] [axioms: ] [] ⊢ F: thm

There are three elements to the left of the turnstile in the fully printed representation of a theorem: the first two3 comprise the tags component and the third is the standard assumption list. The tag component of a theorem can be extracted by

    tag : thm -> tag

and pretty-printed by

    pp_tag : tag -> pretty

Theories

In §LOGIC a theory is described as a $4$-tuple

$$𝒯 = \langle \mathsf{Struc}_𝒯, \mathsf{Sig}_𝒯, \mathsf{Axioms}_𝒯, \mathsf{Theorems}_𝒯\rangle $$

where

  1. $\mathsf{Struc}_𝒯$ is the type structure of 𝒯;
  2. $\mathsf{Sig}_𝒯$ is the signature of 𝒯;
  3. $\mathsf{Axioms}_𝒯$ is the set of axioms of 𝒯;
  4. $\mathsf{Theorems}_𝒯$ is the set of theorems of 𝒯.

In the implementation of HOL, theories are structured hierarchically to represent sequences of extensions called segments of an initial theory called min. A theory segment is not really a logical concept, but rather a means of representing theories in the HOL system. Each segment records some types, constants, axioms and theorems, together with pointers to other segments called its parents. The theory represented by a segment is obtained by taking the union of all the types, constants, axioms and theorems in the segment, together with the types, constants, axioms and theorems in all the segments reachable by following pointers to parents. This collection of reachable segments is called the ancestry of the segment.

ML functions for theory operations

A typical piece of work with the HOL system consists in a number of sessions. In the first of these, a new theory, 𝒯 say, is created by importing some existing theory segments, making a number of definitions, and perhaps proving and storing some theorems in the current segment. Then the current segment (named $\mathit{name}$ say) is exported. The concrete result will be an ML module $\mathsf{name}$Theory whose contents is the current theory segment created during the session and whose ancestry represents the desired logical theory 𝒯. Subsequent work sessions can access the definitions and theorems of 𝒯 by importing $name$Theory; this avoids having to load the tools and replay the proofs that created $name$Theory in the first place.

The naming of data in theories is based on the names given to segments. Specifically an axiom, definition, specification or theorem is accessed by an ML long identifier $\mathit{thy}$Theory.$\mathit{name}$, where $\mathit{thy}$ is the name of the theory segment current when the item was declared and $\mathit{name}$ is a specific name supplied by the user (see the functions new_axiom, new_definition, below). Different items can have the same specific name if the associated segment is different. Thus each theory segment provides a separate namespace of ML bindings of HOL items.

Various additional pieces of information are stored in a theory segment, including the parsing status of the constants (e.g., whether they are infixes or binders).

Determining the Context

There is always a \emph{current theory} which is the theory represented by the current theory segment together with its ancestry. The name of the current theory segment is returned by the ML function:

    current_theory : unit -> string

When an interactive HOL session begins, some theories will already be in the logical context. The exact set of theories in context will vary. If hol --bare is used, then only min and bool will be loaded. When the hol executable is used, a richer context is loaded.

The exact set of theories loaded can be determined with the ancestry command.

    ancestry : string -> string list

This function provides a general mechanism for examining the structure of the theory hierarchy. The argument is the name of a theory (or "-" as an abbreviation for the current theory), to which ancestry will respond with a list of the argument's ancestors in the theory hierarchy.

> ancestry "-";
val it =
   ["ConseqConv", "quantHeuristics", "patternMatches", "ind_type",
    "divides", "While", "cv", "reduce", "one", "sum", "option",
    "quotient", "pair", "combin", "sat", "normalForms", "relation",
    "min", "bool", "marker", "num", "prim_rec", "arithmetic",
    "numeral", "basicSize", "numpair", "pred_set", "list",
    "rich_list", "indexedLists", "hol"]: string list

Creating a Theory Segment

A new theory segment is created by a call to new_theory.

    new_theory : string -> unit

This allocates a new ‘area’ where subsequent theory operations take effect.

A call to new_theory "$\mathit{name}$" fails if:

  • $\mathit{name}$ is not an alphanumeric starting with a letter; or

  • there is a theory already named $\mathit{name}$ in the ancestry of the current segment.

On startup, the current theory segment of HOL is named scratch, which is an empty theory, having a useful collection of theories in its ancestry. Typically, a user would begin by loading whatever extra logical context is required for the work at hand.

The current theory segment acts as a kind of scratchpad. Elements stored in the current segment may be overwritten by subsequent additions, or deleted outright. Any theory elements built from overwritten or deleted elements are held to be out-of-date, and will not be included in the theory when it is finally exported. Out-of-date constants and types are detected by the HOL printer, which will print them surrounded by odd-looking syntax to alert the user.

In contrast to the current segment, (proper) ancestor segments may not be altered.

Loading Prebuilt Theories

Since HOL theories are represented by ML modules, when one is working interactively, one can import an existing theory segment by importing the corresponding module.

    load : string -> unit

\noindent Executing load "$\mathit{name}$Theory" imports the first occurrence of $\mathit{name}$Theory to be found along the loadPath into the session. Any unloaded ancestors of $\mathit{name}$ will be loaded before loading of $\mathit{name}$Theory continues. Note that load can not be used in ML files that are to be compiled; it can only be used in the interactive system.

Adding to the Current Theory

The following ML functions add types and terms to the current theory segment. In typical usage, these functions will not be needed since higher-level definition facilities will invoke these as necessary. However, these functions can be useful for those writing proof tools and derived definition principles.

    new_type : string * int -> unit

Executing new_type("$\mathit{op}$", $n$) makes $\mathit{op}$ a new $n$-ary type operator in the current theory. If $\mathit{op}$ is not an allowed name for a type, a warning will be issued.

    new_constant : string * hol_type -> unit

Executing new_constant("$c$", $\sigma$) makes $c_{\sigma'}$ a new constant of the current theory, for all $c_{\sigma'}$ where $\sigma'$ is an instance of $\sigma$. The type $\sigma$ is called the generic type of $c$. If $c$ is not an allowed name for a constant, a warning will be issued.

    new_axiom : string * term -> thm

Executing new_axiom("$\mathit{name}$",$t$) declares the sequent ([],$t$) to be an axiom of the current theory with name $\mathit{name}$. Failure occurs if:

  • the type of $t$ is not bool; or
  • $t$ contains out-of-date constants or types, $i.e.$, constants or types that have been re-declared after $t$ was built.

Once a theorem has been proved, it can be saved with the function

    save_thm : string * thm -> thm

Evaluating save_thm("$\mathit{name}$",$\mathit{th}$) will save the theorem $\mathit{th}$ with name $\mathit{name}$ in the current theory segment. In addition, various tools can be primed to pay particular attention to saved theorems through the use of special attributes. Such attributes are indicated by appending the list of the attribute names to the $\mathit{name}$. Thus, to indicate the simp attribute (for which, see discussion of the stateful simpset in §Stateful Simpset, one can write

    save_thm("name[simp]", th)

Multiple attributes can be listed between the square brackets, separated by commas.

There are also attributes controlling the way in which a theorem is saved, rather than affecting possible consumers of that theorem. The first, local, is used to create theorems that will not be exported to disk, but which are important locally. Such local theorems can have other attributes attached to them, which will have their effect within the given session/script-file. The second, unlisted, makes the theorem more difficult to access. This is appropriate when the theorem is being saved for the consumption of particular tools (which fact might be indicated by the use of other attributes), but the theorem is otherwise uninteresting, and unlikely to be something that a user is going to want to stumble across when searching or surveying theories. Concretely: unlisted theorems do not appear in theory signature files (so: cannot be accessed with the xTheory.thm_name syntax); and unlisted theorems will not be returned as the results of searches made for theorems using the functions in the DB module.

Instead of using the ML function save_thm, a special Theorem syntax is available for use in script files. One can write

   Theorem name = th

to achieve the same effect as an SML declaration

   val name = save_thm("name", th);

Attributes can also be added; for example:

   Theorem name[simp] = th

If one wishes to prove a goal with a tactic (see Chapter 4), and store the resulting theorem, the combination of these actions can be achieved with the store_thm function:

    store_thm : string * term * tactic -> thm

A call to store_thm(name,$t$,tac) results in the application of tac to goal $t$. If the tactic is successful, the resulting theorem is saved under the name name, as before. Also as before, theorem attributes can be added to the name.

Finally, again for use in script files only, there is a Theorem syntax to replace store_thm, reducing the need to write the same name twice, and giving a cleaner appearance. One can write

   Theorem name[attr1,attr2,...]:
     ...goal statement...
   Proof
     ...tactic...
   QED

The goal statement in this form is not an arbitrary term value, but must use the surface syntax used by the system parser (see §Quotations, Parsing and Printing). For example:

   Theorem IMP_CLAUSE[simp]:
     !p. (p ==> p) <=> T
   Proof
     rpt strip_tac
   QED

(The tactic above is shown to illustrate the Proof/QED shape of the form rather than the proof itself; closing this goal will need a little more than rpt strip_tac.) Note further that the Proof and QED keywords must occur in the leftmost column of the script file so that the parser can know when the term and tactic arguments terminate.

Instead of writing Theorem foo[local,...], one can write Triviality foo[...]. NB: This form has been deprecated and will be removed in a future release.

The choice to use Theorem syntax, or to use store_thm or save_thm directly is a matter of users' aesthetic preference. But recall: if writing library code to prove and store theorems, the underlying store_thm must be used, as the special treatment of the Theorem keyword is only available in script files.

Exporting a Theory

Once a theory segment has been constructed, it can be written out to a file, which, after compilation, can be imported into future sessions.

    export_theory : unit -> unit

When export_theory is called, all out-of-date entities are removed from the current segment. Also, the parenthood of the theory is computed. The current theory segment is written to a file nameTheory.sml, and the file nameTheory.sig, which documents the contents of name, is also written. Notice that the exported theory is not compiled by HOL. That is left to an external tool, Holmake (see the section on Holmake), which maintains dependencies among collections of HOL theory segments.

ML functions for accessing theories

The arguments of ML type string to new_axiom, new_definition, etc., are the names of the corresponding axioms and definitions. These names are used when accessing theories with the functions axiom, definition, etc., described below.

The current theory can be extended by adding new parents, types, constants, axioms and definitions. Theories that are in the ancestry of the current theory cannot be extended in this way; they can be thought of as frozen.

There are various functions for loading the contents of theory files:

    parents : string -> string list
    types : string -> (string * int) list
    constants : string -> term list

The first argument is the name of a theory (which must be in the ancestry of the current theory segment); the result is a list of the components of the theory. The name of the current theory can be abbreviated by "-". For example, parents "-" returns the parents of the current theory.

In the case of types a list of name-arity pairs is returned. Individual axioms, definitions and theorems can be read from the current theory using the following ML functions:

    axiom : string -> thm
    definition : string -> thm
    theorem : string -> thm

The first argument is the user supplied name of the axiom, definition or theorem in the current theory. Further, a list of all of a theory's axioms, definitions and theorems can be retrieved with the ML functions:

    axioms : string -> (string * thm) list
    definitions : string -> (string * thm) list
    theorems : string -> (string * thm) list

The contents of the current theory can be printed in a readable format using the function print_theory.

Functions for creating definitional extensions

There are three kinds of definitional extensions: constant definitions, constant specifications and type definitions.

Constant Definitions

In §LOGIC a constant definition over a signature $\Sigma_{\Omega}$ is defined to be an equation, i.e., a formula of the form $c_{\sigma}=t_{\sigma}$, such that:

  1. $c$ is not the name of any constant in $\Sigma_{\Omega}$;
  2. $t_{\sigma}$ is a closed term in $\mathsf{Terms}_{\Sigma_{\Omega}}$;
  3. all the type variables occurring in $t_{\sigma}$ occur in $\sigma$.

In HOL, definitions can be slightly more general than this, in that an equation: $$ c\ v_1\ \cdots\ v_n\ =\ t $$ is allowed to be a definition where $v_1$, $\dots$, $v_n$ are variable structures (i.e., tuples of distinct variables). Such an equation is logically equivalent to: $$ c\ =\ \lambda v_1\ \cdots\ v_n.\ t $$ which is a definition in the sense of §LOGIC if (i), (ii) and (iii) hold.

The following ML function creates a new definition in the current theory.

    new_definition : string * term -> thm

Evaluating new_definition("name", “$c\ v_1\ \cdots\ v_n\ =\ t$”) declares the sequent $(\{\},\ c = \lambda v_1\ \cdots\ v_n.\ t)$ to be a constant definition of the current theory. The name associated with the definition in this theory is name. Failure occurs if:

  1. $t$ contains free variables that are not in any of the variable structures $v_1$, $\dots$, $v_n$ (this is equivalent to requiring $\lambda v_1\ \cdots\ v_n.\ t$ to be a closed term);
  2. there is a type variable in $v_1$, $\dots$, $v_n$ or $t$ that does not occur in the type of $c$.

Constant Specifications

In §LOGIC a constant specification for a theory $\mathcal{T}$ is defined to be a pair: $$ \langle (c_1,\ldots,c_n),\ \forall {x_1}_{\sigma_1} \cdots {x_n}_{\sigma_n}.\ t_{\mathtt{bool}} \rangle $$ such that:

  1. $c_1$, $\dots$, $c_n$ are distinct names;
  2. $\forall {x_1}_{\sigma_1} \cdots {x_n}_{\sigma_n}.\ t_{\mathtt{bool}} \in \mathsf{Terms}_{\mathcal{T}}$;
  3. $tyvars(\forall {x_1}_{\sigma_1} \cdots {x_n}_{\sigma_n}.\ t_{\mathtt{bool}}) \subseteq tyvars(\sigma_i)$ for $1 \leq i \leq n$;
  4. $\exists {x_1}_{\sigma_1}\ \cdots\ {x_n}_{\sigma_n}.\ t \in \mathsf{Theorems}_{\mathcal{T}}$.

The following ML function is used to make constant specifications in the HOL system.

    new_specification : string * string list * thm -> thm

Evaluating:

   new_specification("name", ["c1", ..., "cn"],
                     |- ?x1 ... xn. t[x1, ..., xn])

simultaneously introduces new constants named $c_1$, $\dots$, $c_n$ satisfying the property: $$ \vdash t[c_1, \ldots, c_n] $$ This theorem is stored, with name name, as a definition in the current theory segment. A call to new_specification fails if:

  1. the theorem argument has a non-empty assumption list;
  2. there are free variables in the theorem argument;
  3. $c_1$, $\dots$, $c_n$ are not distinct variables;
  4. the type of some $c_i$ does not contain all the type variables which occur in the term \x1 ... xn. t[x1, ..., xn].

Type Definitions

In §LOGIC it is explained that defining a new type $(\alpha_1,\ldots,\alpha_n)\mathit{op}$ in a theory $\mathcal{T}$ consists of introducing $\mathit{op}$ as a new $n$-ary type operator and $$ \vdash \exists f_{(\alpha_1,\ldots,\alpha_n)\mathit{op} \to \sigma}.\ \mathsf{TyDef}\ p\ f $$ as a new axiom, where $p$ is a predicate characterizing a non-empty subset of an existing type $\sigma$. Formally, a type definition for a theory $\mathcal{T}$ is a 3-tuple $$ \langle \sigma,\ (\alpha_1,\ldots,\alpha_n)\mathit{op},\ p_{\sigma\to\mathtt{bool}} \rangle $$ where:

  1. $\sigma \in \mathsf{Types}_{\mathcal{T}}$ and $tyvars(\sigma) \in \{\alpha_1, \ldots, \alpha_n\}$;
  2. $\mathit{op}$ is not the name of a type constant in $\mathsf{Struc}_{\mathcal{T}}$;
  3. $p \in \mathsf{Terms}_{\mathcal{T}}$ is a closed term of type $\sigma\to\mathtt{bool}$ and $tyvars(p) \subseteq \{\alpha_1, \ldots, \alpha_n\}$;
  4. $\exists x_{\sigma}.\ p\ x \subseteq \mathsf{Theorems}_{\mathcal{T}}$.

The following ML function makes a type definition in the HOL system.

    new_type_definition : string * thm -> thm

If $t$ is a term of type $\sigma$->bool containing $n$ distinct type variables, then evaluating:

   new_type_definition("op", |- ?x. t x)

results in op being declared as a new $n$-ary type operator characterized by the definitional axiom:

   |- ?rep. TYPE_DEFINITION t rep

which is stored as a definition with the automatically generated name op_TY_DEF. The constant TYPE_DEFINITION is defined in the theory bool by:

   |- TYPE_DEFINITION (P:'a->bool) (rep:'b->'a) =
       (!x' x''. (rep x' = rep x'') ==> (x' = x'')) /\
       (!x. P x = (?x'. x = rep x'))

Executing new_type_definition("op", |- ?$x$. $t\ x$) fails if:

  1. $t$ does not have a type of the form $\sigma$->bool.

Defining Bijections

The result of a type definition using new_type_definition is a theorem which asserts only the existence of a bijection from the type it defines to the corresponding subset of an existing type. To introduce constants that in fact denote such a bijection and its inverse, the following ML function is provided:

    define_new_type_bijections :
      {ABS: string, REP: string, name: string, tyax: thm} -> thm

This function takes a record {ABS, REP, name, tyax}. The tyax argument must be a definitional axiom of the form returned by new_type_definition. The name argument is the name under which the constant definition (a constant specification, in fact) made by define_new_type_bijections will be stored in the current theory segment, and the ABS and REP arguments are user-specified names for the two constants that are to be defined. These constants are defined so as to denote mutually inverse bijections between the defined type, whose definition is given by the supplied theorem, and the representing type of this defined type.

Evaluating:

  define_new_type_bijections
       {name="name", ABS="abs", REP="rep",
        tyax = |- ?rep:newty->ty. TYPE_DEFINITION P rep}

automatically defines two new constants abs:ty->newty and rep:ty->newty such that:

   |- (!a. abs(rep a) = a) /\ (!r. P r = (rep(abs r) = r))

This theorem, which is the defining property for the constants abs and rep, is stored under the name "name" in the current theory segment. It is also the value returned by define_new_type_bijections. The theorem states that abs is the left inverse of rep and — for values satisfying P — that rep is the left inverse of abs.

A call to define_new_type_bijections name abs rep th fails if:

  1. th is not a theorem of the form returned by new_type_definition.

Properties of Type Bijections

The following ML functions are provided for proving that the bijections introduced by define_new_type_bijections are injective (one-to-one) and surjective (onto):

    prove_rep_fn_one_one : thm -> thm
    prove_rep_fn_onto : thm -> thm
    prove_abs_fn_one_one : thm -> thm
    prove_abs_fn_onto : thm -> thm

The theorem argument to each of these functions must be a theorem of the form returned by define_new_type_bijections:

   |- (!a. abs(rep a) = a) /\ (!r. P r = (rep(abs r) = r))

If th is a theorem of this form, then evaluating prove_rep_fn_one_one th proves that the function rep is one-to-one, and returns the theorem:

   |- !a a'. (rep a = rep a') = (a = a')

Likewise, prove_rep_fn_onto th proves that rep is onto the set of values that satisfy P:

   |- !r. P r = (?a. r = rep a)

Evaluating prove_abs_fn_one_one th proves that abs is one-to-one for values that satisfy P, and returns the theorem:

   |- !r r'. P r ==> P r' ==> ((abs r = abs r') = (r = r'))

And evaluating prove_abs_fn_onto th proves that abs is onto, returning the theorem:

   |- !a. ?r. (a = abs r) /\ P r

All four functions will fail if applied to any theorem that does not have the form of a theorem returned by define_new_type_bijections. None of these functions saves anything in the current theory.


  1. Note that ‘hypotheses’ and ‘conclusion’ are also used for the components of sequents.

  2. This is a simplification: in fact the various axioms are an extension of the basic logic.

  3. Tags are also used for tracking the use of axioms in proofs.

Derived Inference Rules

In this section, HOL proofs and theorems are made concrete. The notion of proof is defined abstractly in the manual §LOGIC: a proof of a sequent $(\Gamma,t)$ from a set of sequents $\Delta$ (with respect to a deductive system $\mathcal{D}$) was defined to be a chain of sequents culminating in $(\Gamma,t)$, such that every element of the chain either belongs to $\Delta$ or else follows from $\Delta$ and earlier elements of the chain by deduction. The notion of a theorem was also defined in §LOGIC: a theorem of a deductive system is a sequent that follows from the empty set of sequents by deduction; i.e., it is the last element of a proof in the deductive system from the empty set of sequents.

The deductive system of HOL was sketched in Section§Rules, where the eight families of primitive inferences making up the deductive system were specified by diagrams. It was explained that these families of inferences are represented in HOL via ML functions, and that theorems are represented by an ML abstract type called thm. The eight ML functions corresponding to the inferences are operations of the type thm, and each of the eight returns a value of type thm. It was explained that the type thm has primitive destructors, but no primitive constructor; and that, in that way, the logic is protected against the computation of theorems except by functions representing primitive inferences, or compositions of these.

Finally, the primitive HOL logic was supplemented by three primitive constants and four axioms, to form the basic logic. The primitive inferences, together with the primitive constants, the five axioms, and a collection of definitions, give a starting point for constructing proofs, and hence computing theorems. However, proving even the simplest theorems from this minimal basis costs considerable effort. The basis does not immediately provide the transitivity of equality, for example, or a means of universal quantification; both of these themselves have to be derived.

Simple Derivations

As an illustration of a proof in HOL, the following chain of theorems forms a proof (from the empty set, in the HOL deductive system), for the particular terms $\mathtt{t}_1$ and $\mathtt{t}_2$, both of HOL type :bool

  1. $t_1 \Rightarrow t_2 \vdash t_1 \Rightarrow t_2$

  2. $t_1\; |- t_1$

  3. $t_1 \Rightarrow t_2, \; t_1 \vdash t_2$

That is, the third theorem follows from the first and second.

In the session below, the proof is performed in the HOL system, using the ML functions ASSUME and MP.

> show_assums := true;
val it = (): unit

> val th1 = ASSUME ``t1 ==> t2``
val th1 =  [t1 ⇒ t2] ⊢ t1 ⇒ t2: thm

> val th2 = ASSUME ``t1:bool``
val th2 =  [t1] ⊢ t1: thm

> MP th1 th2;
val it =  [t1, t1 ⇒ t2] ⊢ t2: thm

In the following, the Count.inferences function is used to count the number of primitive inferences performed in the course of applying the function to the argument. In the first invocation, this means that only the modus ponens step is counted. We create an artificial function to see the count of all three inference steps in the second interaction:

> Count.inferences (MP (ASSUME ``t1 ==> t2``)) (ASSUME ``t1:bool``);
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 1; Total: 1
val it =  [t1, t1 ⇒ t2] ⊢ t2: thm

> fun f () = MP (ASSUME ``t1 ==> t2``) (ASSUME ``t1:bool``);
val f = fn: unit -> thm

> Count.inferences f ();
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 3; Total: 3
val it =  [t1, t1 ⇒ t2] ⊢ t2: thm

Each of the three inference steps of the abstract proof corresponds to the application of an ML function in the performance of the proof in HOL; and each of the ML functions corresponds to a primitive inference of the deductive system.

It is worth emphasising that, in either case, every primitive inference in the proof chain is made, in the sense that for each inference, the corresponding ML function is evaluated. That is, HOL permits no short-cut around the necessity of performing complete proofs. The short-cut provided by derived inference rules (as implemented in ML) is around the necessity of specifying every step; something that would be impossible for a proof of any length. It can be seen from this that the derived rule, and its representation as an ML function, is essential to the HOL methodology; theorem proving would be otherwise impossible.

There are, of course, an infinite number of proofs of the form shown in the example that can be conducted in HOL: one for every pair of :bool-typed terms. Moreover, every time a theorem of the form

$$t_1 \Rightarrow \ t_2, \ t_1 \ \vdash \ t_2$$

is required, its proof must be constructed anew. To capture the general pattern of inference, an ML function can be written to implement an inference rule as a derivation from the primitive inferences. Abstractly, a derived inference rule is a rule that can be justified on the basis of the primitive inference rules (and/or the axioms). In the present case, the rule required undischarged assumptions. It is specified for HOL by

      Γ |- t1 ==> t2
     -----------------
      Γ ∪ {t1} |-  t2

This general rule is valid because, from a HOL theorem of the form $\Gamma \vdash t_1 \Rightarrow t_2$, the theorem $\Gamma \cup\{t_1\} \vdash t_2$ can be derived as in the specific instance above. The rule can be implemented in ML as a function (UNDISCH, say) that calls the appropriate sequence of primitive inferences. The ML definition of UNDISCH is simply

> fun UNDISCH th = MP th (ASSUME $ fst $ dest_imp $ concl th);
val UNDISCH = fn: thm -> thm

This code provides a function that maps a theorem to a theorem; that is, performs proofs in HOL. The following session illustrates the use of the derived rule on a consequence of the axiom IMP_ANTISYM_AX. (The inferences are counted. Assume that the printing of theorems has been adjusted as above and th is bound as shown below:

> val th = SPEC ``t2:bool`` $ SPEC ``t1:bool`` IMP_ANTISYM_AX;
val th =  [] ⊢ (t1 ⇒ t2) ⇒ (t2 ⇒ t1) ⇒ (t1 ⇔ t2): thm

> Count.inferences UNDISCH th;
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 2; Total: 2
val it =  [t1 ⇒ t2] ⊢ (t2 ⇒ t1) ⇒ (t1 ⇔ t2): thm

> Count.inferences UNDISCH it;
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 2; Total: 2
val it =  [t1 ⇒ t2, t2 ⇒ t1] ⊢ t1 ⇔ t2: thm

Each successful application of UNDISCH to a theorem invokes an application of ASSUME, followed by an application of MP; UNDISCH constructs the 2-step proof for any given theorem of the appropriate form. As can be seen, it relies on the class of ML functions that access HOL syntax: in particular, concl to produce the conclusion of the theorem, dest_imp to separate the implication, and the selector fst to choose the antecedent.

This particular example is very simple, but a derived inference rule can perform proofs of arbitrary length. It can also make use of previously defined rules. In this way, the normal inference patterns can be developed much more quickly and easily; transitivity, generalization, and so on, support the familiar patterns of inference.

A number of derived inference rules are pre-defined when the HOL system is entered (UNDISCH is one of the first). In Section, the abstract derivations are given for the pre-defined rules that reflect the more usual inference patterns of the predicate (and lambda) calculi. Like those shown, some of the pre-defined derived rules in HOL generate relatively short proofs. Others invoke thousands of primitive inferences, and clearly save a great deal of effort. Furthermore, rules can be defined by the user to make still larger steps, or to implement more specialized patterns.

All of the pre-defined derived rules in HOL are described in §REFERENCE.

Derivation of the Standard Rules

The HOL system provides all the standard introduction and elimination rules of the predicate calculus pre-defined as derived inferences. It is these derived rules, rather than the primitive rules, that one normally uses in practice. In this section, the derivations of some of the standard rules are given, in sequence. These derivations only use the axioms and definitions in the theory bool (see Section, the eight primitive inferences of the HOL logic, and inferences defined earlier in the sequence.

Theorems, in accordance with the definition given at the beginning of this chapter, are treated as rules without hypotheses; thus the derivation of a theorem resembles the derivation of a rule except in not having hypotheses. (The derivation of TRUTH, Section, is the only example given of this, but there are several others in HOL.) There are also some rules that are intrinsically more general than theorems. For example, for any two terms $t_1$ and $t_2$, the theorem $\vdash(\lambda{x}.t_1)t_2 = t_1[t_2/x]$ follows by the primitive rule BETA_CONV. The rule BETA_CONV returns a theorem for each pair of terms $t_1$ and $t_2$, and is therefore equivalent to an infinite family of theorems. No single theorem can be expressed in the HOL logic that is equivalent to BETA_CONV. See Chapter for further discussion of this point. Note that UNDISCH is not a rule of this sort, as it can, in fact, be expresed as a theorem.

For each derivation given below, there is an ML function definition in the HOL system that implements the derived rule as a procedure in ML. The actual implementation in the HOL system differs in some cases from the derivations given here, since the system code has been optimised for improved performance.

In addition, for reasons that are mostly historical, not all the inferences that are derived in terms of the abstract logic are actually derived in the current version of the HOL system. That is, there are currently a number of rules that are installed in the system on an “axiomatic” basis, all of which should be derived by explicit inference. These rules' status does not actually compromise the consistency of the logic. In effect, the existing HOL system has a deductive system more comprehensive than the one presented abstractly, but the model outlined in §LOGIC would easily extend to cover it. The derivations that follow consist of sequences of numbered steps each of which

  1. is an axiom, or
  2. is a hypothesis of the rule being derived, or
  3. follows from preceding steps by a rule of inference (either primitive or previously derived).

Adding an assumption

   ADD_ASSUM : term -> thm -> thm

   Γ |- t
  ---------------
   Γ ∪ {t'} |- t

 

1. $t'\vdash t'$[ASSUME]
2. $\Gamma\vdash t$[Hypothesis]
3. $\Gamma\vdash t' \Rightarrow t$[DISCH 2]
4. $\Gamma,\ t'\vdash t$[MP 3,1]

Undischarging

   UNDISCH : thm -> thm

   Γ |- t1 ==> t2
  ---------------
   Γ ∪ {t1} |- t2

 

1. $t_1\vdash t_1$[ASSUME]
2. $\Gamma\vdash t_1\Rightarrow t_2$[Hypothesis]
3. $\Gamma,\ t_1\vdash t_2$[MP 2,1]

Symmetry of equality

   SYM : thm -> thm

   Γ |- t1 = t2
  ---------------
   Γ |- t2 = t1

 

1. $\Gamma\vdash t_1=t_2$[Hypothesis]
2. $\vdash t_1=t_1$[REFL]
3. $\Gamma\vdash t_2=t_1$[SUBST 1,2]

Transitivity of equality

   TRANS : thm -> thm -> thm

   Γ₁ |- t₁ = t₂ ,   Γ₂ |- t₂ = t3
  ---------------------------------
     Γ₁ ∪ Γ₂ |- t₁ = t3

 

1. $\Gamma_2\vdash t_2=t_3$[Hypothesis]
2. $\Gamma_1\vdash t_1=t_2$[Hypothesis]
3. $\Gamma_1\cup\Gamma_2\vdash t_1=t_3$[SUBST 1,2]

Application of a term to a theorem

   AP_TERM : term -> thm -> thm

   Γ |- t₁ = t₂
  ------------------
   Γ |- t t₁ = t t₂

 

1. $\Gamma\vdash t_1=t_2$[Hypothesis]
2. $\vdash t\ t_1 = t\ t_1$[REFL]
3. $\Gamma\vdash t\ t_1 = t\ t_2$[SUBST 1,2]

Application of a theorem to a term

   AP_THM : thm -> conv

   Γ |- t₁ = t₂
  ------------------
   Γ |- t₁ t = t₂ t

 

1. $\Gamma\vdash t_1=t_2$[Hypothesis]
2. $\vdash t_1\ t = t_1\ t$[REFL]
3. $\Gamma\vdash t_1\ t = t_2\ t$[SUBST 1,2]

Modus Ponens for equality

   EQ_MP : thm -> thm -> thm

   Γ₁ |- t₁ = t₂ ,   Γ₂ |- t₁
  ---------------------------
     Γ₁ ∪ Γ₂ |- t₁ = t₂

 

1. $\Gamma_1\vdash t_1=t_2$[Hypothesis]
2. $\Gamma_2\vdash t_1$[Hypothesis]
3. $\Gamma_1\cup\Gamma_2\vdash t_2$[SUBST 1,2]

Implication from equality

   EQ_IMP_RULE : thm -> thm * thm

          Γ |- t₁ = t₂
  ----------------------------------
   Γ |- t₁ ==> t₂ ,   Γ |- t₂ ==> t₁

 

1. $\Gamma\vdash t_1=t_2$[Hypothesis]
2. $t_1\vdash t_1$[ASSUME]
3. $\Gamma,\ t_1\vdash t_2$[EQ_MP 1,2]
4. $\Gamma\vdash t_1\Rightarrow t_2$[DISCH 3]
5. $\Gamma\vdash t_2=t_1$[SYM 1]
6. $t_2\vdash t_2$[ASSUME]
7. $\Gamma,\ t_2\vdash t_1$[EQ_MP 5,6]
8. $\Gamma\vdash t_2\Rightarrow t_1$[DISCH 7]
9. $\Gamma\vdash t_1\Rightarrow t_2$ and $\Gamma\vdash t_2\Rightarrow t_1$[4,8]

$\mathsf{T}$-introduction

   TRUTH : thm

  ------------
     |- T

 

1. $\vdash \mathsf{T} = ((\lambda x.\; x) = (\lambda x.\; x))$[Definition of T]
2. $\vdash ((\lambda x.\; x) = (\lambda x.\; x)) = \mathsf{T}$[SYM 1]
3. $\vdash (\lambda x.\; x) = (\lambda x.\; x)$[REFL]
4. $\vdash \mathsf{T}$[EQ_MP 2,3]

Equality-with-$\mathsf{T}$ elimination

   EQT_ELIM : thm -> thm

   Γ |- t = T
  -------------
   Γ |- t

 

1. $\Gamma\vdash t = \mathsf{T}$[Hypothesis]
2. $\Gamma\vdash \mathsf{T} = t$[SYM 1]
3. $\vdash \mathsf{T}$[TRUTH]
4. $\Gamma\vdash t$[EQ_MP 2, 3]

Specialization ($\forall$-elimination)

   SPEC : term -> thm -> thm

   Γ |- ∀x. t
  -------------------
   Γ |- t[t'/x]

 

The notation $t[t'/x]$ denotes the result of replacing each free occurrence of $x$ in $t$ by $t'$. Renaming ensures that no free variables in a resulting occurrence of $t'$ will become bound.

1. $\vdash \forall = (\lambda P.\; P = (\lambda x. \mathsf{T}))$[INST_TYPE on definition of $\forall$]
2. $\Gamma\vdash \forall(\lambda x.\;t)$[Hypothesis]
3. $\Gamma\vdash (\lambda{P}.\; P = (\lambda{x}.\;\mathsf{T})) (\lambda{x}.\;t)$[SUBST 1,2]
4. $\vdash (\lambda{P}.\; P= (\lambda{x}.\;\mathsf{T}))(\lambda{x}.\;t) = ((\lambda{x}.\;t) = (\lambda{x}.\;\mathsf{T}))$[BETA_CONV]
5. $\Gamma\vdash (\lambda{x}.\;t)=(\lambda{x}.\;\mathsf{T})$[EQ_MP 4,3]
6. $\Gamma\vdash (\lambda{x}.\;t)\ t' = (\lambda{x}.\;\mathsf{T})\ t'$[AP_THM 5]
7. $\vdash (\lambda{x}.\;t)\ t' = t[t'/x]$[BETA_CONV]
8. $\Gamma\vdash t[t'/x] = (\lambda{x}.\;t)\ t'$[SYM 7]
9. $\Gamma\vdash t[t'/x] = (\lambda{x}.\;\mathsf{T})\ t'$[TRANS 8,6]
10. $\vdash (\lambda{x}.\;\mathsf{T})\ t' = \mathsf{T}$[BETA_CONV]
11. $\Gamma\vdash t[t'/x] = \mathsf{T}$[TRANS 9,10]
12. $\Gamma\vdash t[t'/x]$[EQT_ELIM 11]

Equality-with-$\mathsf{T}$\ introduction

   EQT_INTRO : thm -> thm

   Γ |- t
  -------------
   Γ |- t = T

 

1. $\vdash\forall{b_1\ b_2}.\; (b_1\Rightarrow b_2) \Rightarrow (b_2\Rightarrow b_1) \Rightarrow(b_1=b_2)$[Axiom]
2. $\vdash\forall{b_2}.\; (t\Rightarrow b_2)\Rightarrow(b_2\Rightarrow t)\Rightarrow(t=b_2)$[SPEC 1]
3. $\vdash(t\Rightarrow\mathsf{T})\Rightarrow(\mathsf{T}\Rightarrow t)\Rightarrow(t=\mathsf{T})$[SPEC 2]
4. $\vdash\mathsf{T}$[TRUTH]
5. $\vdash t\Rightarrow\mathsf{T}$[DISCH 4]
6. $\vdash(\mathsf{T}\Rightarrow t)\Rightarrow(t=\mathsf{T})$[MP 3,5]
7. $\Gamma \vdash t$[Hypothesis]
8. $\Gamma\vdash\mathsf{T}\Rightarrow t$[DISCH 7]
9. $\Gamma\vdash t=\mathsf{T}$[MP 6,8]

Generalization ($\forall$-introduction)

   GEN : term -> thm -> thm

   Γ |- t
  ---------------
   Γ |- ∀x. t

Restriction: variable $x$ can not occur free in $\Gamma$.

Remark: The conventional notation for universal quantification, i.e. $\forall x.\; t$, is, in HOL, surface syntax for the underlying *term structure $\forall(\lambda{x}.\;t)$. This underlying structure is used to give clarity to some derivations in the following.

 

1. $\Gamma\vdash t$[Hypothesis]
2. $\Gamma\vdash t = \mathsf{T}$[EQT_INTRO 1]
3. $\Gamma\vdash(\lambda{x}.\;t)=(\lambda{x}.\;\mathsf{T})$[ABS 2]
4. $\vdash \forall(\lambda{x}.\;t) = \forall(\lambda{x}.\;t)$[REFL]
5. $\vdash \forall = (\lambda{P}.\;P =(\lambda{x}.\;\mathsf{T}))$[INST_TYPE on definition of $\forall$]
6. $\vdash\forall(\lambda{x}.\;t)=(\lambda{P}.\;P=(\lambda{x}.\;\mathsf{T}))(\lambda{x}.\;t)$[SUBST 5,4]
7. $\vdash(\lambda{P}.\;P=(\lambda{x}.\;\mathsf{T}))(\lambda{x}.\;t)=((\lambda{x}.\;t) = (\lambda{x}.\;\mathsf{T}))$[BETA_CONV]
8. $\vdash\forall(\lambda{x}t) = ((\lambda{x}.\;t)=(\lambda{x}.\;\mathsf{T}))$[TRANS 6,7]
9. $\vdash((\lambda{x}.\;t)=(\lambda{x}.\;\mathsf{T})) = \forall(\lambda{x}.\;\mathsf{T})$[SYM 8]
10. $\Gamma\vdash\forall(\lambda{x}.\;t)$[EQ_MP 9,3]

Simple $\alpha$-conversion

   SIMPLE_ALPHA

  ----------------------------------
   |- (λx₁. t) x₁ = (λx₂. t) x₂

Restriction: neither $x_1$ nor $x_2$ may occur free in $t$.

Remark: SIMPLE_ALPHA is not actually defined in the HOL system, as it is subsumed by other rules. It is included here just to support a later derivation in this section.

 

1. $\vdash(\lambda{x_1}.\;t\ x_1)\ x = t\ x$[BETA_CONV]
2. $\vdash(\lambda{x_2}.\;t\ x_2)\ x = t\ x$[BETA_CONV]
3. $\vdash t\ x = (\lambda{x_2}.\;t\ x_2)\ x$[SYM 2]
4. $\vdash (\lambda{x_1}.\;t\ x_1)\ x = (\lambda{x_2}.\;t\ x_2)\ x$[TRANS 1,3]
5. $\vdash(\lambda{x}(\lambda{x_1}.\;t\ x_1)\ x) = (\lambda{x}.\;(\lambda{x_2}.\;t\ x_2)\ x)$[ABS 4]
6. $\vdash\forall{f}.\;(\lambda{x}.\;f\ x) = f$[Type-instantiate ETA_AX]
7. $\vdash(\lambda{x}(\lambda{x_1}.\;t\ x_1)x) = \lambda{x_1}.\;t\ x_1$[SPEC 6]
8. $\vdash(\lambda{x}(\lambda{x_2}.\;t\ x_2)x) = \lambda{x_2}.\;t\ x_2$[SPEC 6]
9. $\vdash (\lambda{x_1}.\;t\ x_1) = (\lambda{x}.\;(\lambda{x_1}.\;t\ x_1)x)$[SYM 7]
10. $\vdash (\lambda{x_1}.\;t\ x_1) = (\lambda{x}.\;(\lambda{x_2}.\;t\ x_2)x)$[TRANS 9,5]
11. $\vdash(\lambda{x_1}.\;t\ x_1)=(\lambda{x_2}.\;t\ x_2)$[TRANS 10,8]

$\eta$-conversion

   ETA_CONV : conv

  -------------------
   |- (λx'. t x') = t

Restriction: $x'$ does not occur free in $t$.

Remark: we use $x'$ rather than just $x$ to motivate the use of SIMPLE_ALPHA in the derivation below.

Remark: In the HOL system, the type abbreviation conv = term -> thm describes SML functions that, when given a term $t_1$, return a theorem $\vdash t_1 = t_2$.

 

1. $\vdash\forall{f}.\;(\lambda{x}.\;f\ x) = f$[Type-instantiate ETA_AX]
2. $\vdash(\lambda{x}.\;t\ x) = t$[SPEC 1]
3. $\vdash(\lambda{x'}.\;t\ x') = (\lambda{x}.\;t\ x)$[SIMPLE_ALPHA]
4. $\vdash(\lambda{x'}.\;t\ x') = t$[TRANS 3,2]

Extensionality

   EXT : thm -> thm

   Γ |- ∀x. t₁ x = t₂ x
  ----------------------
   Γ |- t₁ = t₂

Restriction: $x$ must not occur free in $t_1$ or $t_2$.

 

1. $\Gamma\vdash\forall{x}.\;t_1\ x=t_2\ x$[Hypothesis]
2. $\Gamma\vdash t_1\ x'=t_2\ x'$[SPEC 1 ($x'$ is a fresh var)]
3. $\Gamma\vdash(\lambda{x'}.\;t_1\ x') = (\lambda{x'}.\;t_2\ x')$[ABS 2]
4. $\vdash(\lambda{x'}.\;t_1\ x') = t_1$[ETA_CONV]
5. $\vdash t_1 = (\lambda{x'}.\;t_1\ x')$[SYM 4]
6. $\Gamma\vdash t_1 = (\lambda{x'}.\;t_2\ x')$[TRANS 5,3]
7. $\vdash(\lambda{x'}.\;t_2\ x') = t_2$[ETA_CONV]
8. $\Gamma\vdash t_1=t_2$[TRANS 6,7]

Choice introduction

   SELECT_INTRO : thm -> thm

   Γ |- t₁ t₂
  ----------------
   Γ |- t₁ (ε t₁)

 

1. $\vdash\forall{P\ x}.\;P\ x\Rightarrow P(\varepsilon\; P)$[Type-instantiate SELECT_AX]
2. $\vdash t_1\ t_2 \Rightarrow t_1(\varepsilon\ t_1)$[SPEC 1 (twice)]
3. $\Gamma\vdash t_1\ t_2$[Hypothesis]
4. $\Gamma\vdash t_1(\varepsilon\ t_1)$[MP 2,3]

Choice elimination

   SELECT_ELIM : thm -> term * thm -> thm


   Γ₁ |- t₁ (ε t₁) ,  Γ₂,t₁(v) |- t
  ----------------------------------
   Γ₁ ∪ Γ₂ |- t

Restriction: $v$ occurs nowhere except in the assumption $t_1\ v$ of the second hypothesis.

 

1. $\Gamma_2,\ t_1\ v\vdash t$[Hypothesis]
2. $\Gamma_2\vdash t_1\ v\Rightarrow t$[DISCH 1]
3. $\Gamma_2\vdash\forall{v}.\;t_1\ v\Rightarrow t$[GEN 2]
4. $\Gamma_2\vdash t_1(\varepsilon\ t_1)\Rightarrow t$[SPEC 3]
5. $\Gamma_1\vdash t_1(\varepsilon\ t_1)$[Hypothesis]
6. $\Gamma_1\cup\Gamma_2\vdash t$[MP 4,5]

Existential introduction

   EXISTS : term * term -> thm -> thm

   Γ |- t₁[t₂]
  ---------------
   Γ |- ∃x. t₁[x]

Notation: $t_1[t_2]$ denotes a term $t_1$ with some free occurrences of $t_2$ singled out, and $t_1[x]$ denotes the result of replacing these occurrences of $t_1$ by $x$.

 

1. $\vdash(\lambda{x}.\;t_1[x])t_2= t_1[t_2]$[BETA_CONV]
2. $\vdash t_1[t_2] = (\lambda{x}.\;t_1[x])t_2$[SYM 1]
3. $\Gamma\vdash t_1[t_2]$[Hypothesis]
4. $\Gamma\vdash(\lambda{x}.\;t_1[x])t_2$[EQ_MP 2,3]
5. $\Gamma\vdash(\lambda{x}.\;t_1[x])(\varepsilon(\lambda{x}.\;t_1[x]))$[SELECT_INTRO 4]
6. $\vdash \exists = \lambda{P}.\;P(\varepsilon\ P)$[INST_TYPE on definition of $\exists$]
7. $\vdash\exists(\lambda{x}.\;t_1[x]) = (\lambda{P}.\;P(\varepsilon\ P))(\lambda{x}.\;t_1[x])$[AP_THM 6]
8. $\vdash(\lambda{P}.\;P(\varepsilon\ P))(\lambda{x}.\;t_1[x]) = (\lambda{x}.\;t_1[x])(\varepsilon(\lambda{x}.\;t_1[x]))$[BETA_CONV]
9. $\vdash\exists(\lambda{x}.\;t_1[x]) = (\lambda{x}.\;t_1[x])(\varepsilon(\lambda{x}.\;t_1[x]))$[TRANS 7,8]
10. $\vdash(\lambda{x}.\;t_1[x])(\varepsilon(\lambda{x}.\;t_1[x])) = \exists(\lambda{x}.\;t_1[x])$[SYM 9]
11. $\Gamma\vdash\exists(\lambda{x}.\;t_1[x])$[EQ_MP 10,5]

Existential elimination

   CHOOSE : term * thm -> thm -> thm

   Γ₁ |- ∃x. t[x] ,  Γ₂,t[v] |- t'
  ----------------------------------
   Γ₁ ∪ Γ₂ |- t'

Restrictions: $v$ must not be free in $Γ_1,Γ_2$ or $t$.

 

1. $\vdash \exists = \lambda{P}.\; P(\varepsilon\ P)$[INST_TYPE on definition of $\exists$]
2. $\vdash\exists(\lambda{x}.\;t[x]) = (\lambda{P}.\;P(\varepsilon\ P))(\lambda{x}.\;t[x])$[AP_THM 1]
3. $\Gamma_1\vdash\exists(\lambda{x}t[x])$[Hypothesis]
4. $\Gamma_1\vdash (\lambda{P}.\;P(\varepsilon\ P))(\lambda{x}.\;t[x])$[EQ_MP 2,3]
5. $\vdash(\lambda{P}.\;P(\varepsilon\ P))(\lambda{x}.\;t[x]) = (\lambda{x}.\;t[x])(\varepsilon(\lambda{x}.\;t[x]))$[BETA_CONV]
6. $\Gamma_1\vdash(\lambda{x}.\;t[x])(\varepsilon(\lambda{x}.\;t[x])$[EQ_MP 5,4]
7. $\vdash(\lambda{x}.\;t[x])v = t[v]$[BETA_CONV]
8. $\vdash t[v] =(\lambda{x}.\;t[x])v$[SYM 7]
9. $\Gamma_2,\ t[v]\vdash t'$[Hypothesis]
10. $\Gamma_2\vdash t[v]\Rightarrow t'$[DISCH 9]
11. $\Gamma_2\vdash(\lambda{x}.\;t[x])v\Rightarrow t'$[SUBST 8,10]
12. $\Gamma_2,\ (\lambda{x}.\;t[x])v\vdash t'$[UNDISCH 11]
13. $\Gamma_1\cup\Gamma_2\vdash t'$[SELECT_ELIM 6,12]

Applying a definition to one argument

Given an equation where the right-hand sde is a lambda-abstraction, one can derive an equation characterising the application of the function to a specified argument.

   RIGHT_BETA : thm -> thm

   Γ |- t = (λx. t₁ x) t₂
  ----------------------------------
   Γ |- t = t₁[t₂]

 

1. $\Gamma\vdash t = (\lambda{x}.\; t_1) t_2$[Hypothesis]
2. $\vdash (\lambda{x}.\; t_1) t_2 = t_1[t_2]$[BETA_CONV]
3. $\Gamma\vdash t = t_1[t_2]$[TRANS 1,2]

Applying a definition to multiple arguments

   RIGHT_LIST_BETA : thm -> thm

   Γ |- t = (λx₁…xₙ. t'[x₁,…,xₙ]) t₁ … tₙ
  -------------------------------------------------
   Γ |- t = t'[t₁,…,tₙ]

 

For readability, let $\mathcal{M} = \lambda{x_1\cdots x_n}.\; t'[x_1,\ldots,x_n]$ in the following:

1. $\Gamma\vdash t = \mathcal{M} \;t_1 \cdots t_n$[Hypothesis]
2. $\vdash \mathcal{M} = \mathcal{M}$[REFL]
3. $\vdash \mathcal{M}\;t_1 = \mathcal{M}\;t_1$[AP_THM 2]
4. $\vdash \mathcal{M}\;t_1 = (\lambda{x_2\cdots x_n}.\;t'[t_1,x_2,\ldots,x_n])$[RIGHT_BETA 3]
5. $\vdash \mathcal{M}\;t_1 \cdots t_n = t'[t_1,\ldots,t_n]$[repeat 3,4 for $t_2,...,t_n$]
6. $\Gamma\vdash t = t'[t_1,\ldots,t_n]$[TRANS 1,5]

Conjunction introduction

   CONJ : thm -> thm -> thm

   Γ₁ |- t₁  ,  Γ₂ |- t₂
  -----------------------
   Γ₁ ∪ Γ₂ |- t₁ ∧ t₂

 

0. $\vdash \land = \lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow(b_2\Rightarrow b))\Rightarrow b$[Definition]
1. $\vdash \land\;t_1\;t_2 = (\lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow(b_2\Rightarrow b))\Rightarrow b)\;t_1\;t_2$[AP_THM 0 (twice)]
2. $\vdash t_1\land t_2 = \forall{b}.\;(t_1\Rightarrow(t_2\Rightarrow b))\Rightarrow b$[RIGHT_LIST_BETA 1]
3. $t_1\Rightarrow(t_2\Rightarrow b)\vdash t_1\Rightarrow(t_2\Rightarrow b)$[ASSUME]
4. $\Gamma_1\vdash t_1$[Hypothesis]
5. $\Gamma_1,\ t_1\Rightarrow(t_2\Rightarrow b)\vdash t_2\Rightarrow b$[MP 3,4]
6. $\Gamma_2\vdash t_2$[Hypothesis]
7. $\Gamma_1\cup\Gamma_2,\ t_1\Rightarrow(t_2\Rightarrow b)\vdash b$[MP 5,6]
8. $\Gamma_1\cup \Gamma_2\vdash(t_1\Rightarrow(t_2\Rightarrow b))\Rightarrow b$[DISCH 7]
9. $\Gamma_1\cup \Gamma_2\vdash \forall{b}.\;(t_1\Rightarrow(t_2\Rightarrow b))\Rightarrow b$[GEN 8]
10. $\Gamma_1\cup \Gamma_2\vdash t_1 \land t_2$[EQ_MP (SYM 2),9]

Conjunction elimination

   CONJUNCT1 : thm -> thm
   CONJUNCT2 : thm -> thm

   Γ |- t₁ ∧ t₂
  -----------------------
   Γ |- t₁  ,  Γ |- t₂

 

1. $\vdash \land = \lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow(b_2\Rightarrow b))\Rightarrow b$[Definition]
2. $\vdash t_1\land t_2 = \forall{b}.\;(t_1\Rightarrow(t_2\Rightarrow b))\Rightarrow b$[RIGHT_LIST_BETA 1]
3. $\Gamma\vdash t_1\land t_2$[Hypothesis]
4. $\Gamma\vdash \forall{b}.\;(t_1\Rightarrow(t_2\Rightarrow b))\Rightarrow b$[EQ_MP 2,3]
5. $\Gamma\vdash (t_1\Rightarrow(t_2\Rightarrow t_1))\Rightarrow t_1$[SPEC 4]
6. $t_1\vdash t_1$[ASSUME]
7. $t_1 \vdash t_2\Rightarrow t_1$[DISCH 6]
8. $\vdash t_1\Rightarrow(t_2\Rightarrow t_1)$[DISCH 7]
9. $\Gamma\vdash t_1$[MP 5,8]
10. $\Gamma\vdash (t_1\Rightarrow(t_2\Rightarrow t_2))\Rightarrow t_2$[SPEC 4]
11. $t_2\vdash t_2$[ASSUME]
12. $\vdash t_2\Rightarrow t_2$[DISCH 11]
13. $\vdash t_1\Rightarrow(t_2\Rightarrow t_2)$[DISCH 12]
14. $\Gamma\vdash t_2$[MP 10,13]
15. $\Gamma\vdash t_1$ and $\Gamma\vdash t_2$[9,14]

Right disjunction introduction

   DISJ1 : thm -> conv

   Γ |- t₁
  --------------
   Γ |- t₁ ∨ t₂

 

1. $\vdash \lor = \lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow b)\Rightarrow(b_2\Rightarrow b)\Rightarrow b$[Definition of $\lor$]
2. $\vdash t_1\lor t_2 = \forall{b}.\;(t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[RIGHT_LIST_BETA 1]
3. $\Gamma\vdash t_1$[Hypothesis]
4. $t_1\Rightarrow b\vdash t_1\Rightarrow b$[ASSUME]
5. $\Gamma,\ t_1\Rightarrow b\vdash b$[MP 4,3]
6. $\Gamma,\ t_1\Rightarrow b\vdash(t_2\Rightarrow b)\Rightarrow b$[DISCH 5]
7. $\Gamma\vdash (t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[DISCH 6]
8. $\Gamma\vdash \forall{b}.\;(t_1\Rightarrow b) \Rightarrow(t_2\Rightarrow b)\Rightarrow b$[GEN 7]
9. $\Gamma\vdash t_1\lor t_2$[EQ_MP (SYM 2),8]

Left disjunction introduction

   DISJ2 : term -> thm -> thm

   Γ |- t₂
  --------------
   Γ |- t₁ ∨ t₂

 

1. $\vdash \lor = \lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow b)\Rightarrow(b_2\Rightarrow b)\Rightarrow b$[Definition]
2. $\vdash t_1\lor t_2 = \forall{b}.\;(t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[RIGHT_LIST_BETA 1]
3. $\Gamma\vdash t_2$[Hypothesis]
4. $t_2\Rightarrow b\vdash t_2\Rightarrow b$[ASSUME]
5. $\Gamma,\ t_2\Rightarrow b\vdash b$[MP 4,3]
6. $\Gamma\vdash(t_2\Rightarrow b)\Rightarrow b$[DISCH 5]
7. $\Gamma\vdash (t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[DISCH 6]
8. $\Gamma\vdash \forall{b}.\;(t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[GEN 7]
9. $\Gamma\vdash t_1\lor t_2$[EQ_MP (SYM 2),8]

Disjunction elimination

   DISJ_CASES : thm -> thm -> thm -> thm

   Γ |- t₁ ∨ t₂  ,  Γ₁,t₁ |- t  ,  Γ₂,t₂ |- t
  --------------------------------------------
   Γ ∪ Γ₁ ∪ Γ₂ |- t

 

1. $\vdash \lor =\lambda{b_1\ b_2}.\;\forall{b}.\;(b_1\Rightarrow b)\Rightarrow(b_2\Rightarrow b)\Rightarrow b$[Definition]
2. $\vdash t_1\lor t_2 = \forall{b}.\;(t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[RIGHT_LIST_BETA 1]
3. $\Gamma\vdash t_1\lor t_2$[Hypothesis]
3. $\Gamma\vdash\forall{b}.\;(t_1\Rightarrow b)\Rightarrow(t_2\Rightarrow b)\Rightarrow b$[EQ_MP 2,3]
5. $\Gamma\vdash(t_1\Rightarrow t)\Rightarrow(t_2\Rightarrow t)\Rightarrow t$[SPEC 4]
6. $\Gamma_1,\ t_1\vdash t$[Hypothesis]
7. $\Gamma_1\vdash t_1\Rightarrow t$[DISCH 6]
8. $\Gamma\cup \Gamma_1\vdash (t_2\Rightarrow t)\Rightarrow t$[MP 5,7]
9. $\Gamma_2,\ t_2\vdash t$[Hypothesis]
10. $\Gamma_2\vdash t_2\Rightarrow t$[DISCH 9]
11. $\Gamma\cup \Gamma_1\cup \Gamma_2\vdash t$[MP 8,10]

Classical contradiction rule

   CCONTR : term -> thm -> thm

   Γ, ¬t |- F
  ------------
   Γ |- t

 

1. $\vdash \neg = \lambda{b}.\;b\Rightarrow\mathsf{F}$[Definition]
2. $\vdash \neg t = t\Rightarrow\mathsf{F}$[RIGHT_LIST_BETA 1]
3. $\Gamma,\ \neg t\vdash\mathsf{F}$[Hypothesis]
4. $\Gamma\vdash \neg t\Rightarrow\mathsf{F}$[DISCH 3]
5. $\Gamma\vdash (t\Rightarrow\mathsf{F})\Rightarrow\mathsf{F}$[SUBST 2,4]
6. $t = \mathsf{F}\vdash t = \mathsf{F}$[ASSUME]
7. $\Gamma,\ t=\mathsf{F}\vdash (\mathsf{F}\Rightarrow\mathsf{F})\Rightarrow\mathsf{F}$[SUBST 6,5]
8. $\mathsf{F}\vdash\mathsf{F}$[ASSUME]
9. $\vdash \mathsf{F}\Rightarrow\mathsf{F}$[DISCH 8]
10. $\Gamma,\ t=\mathsf{F}\vdash\mathsf{F}$[MP 7,9]
11. $\vdash \mathsf{F} = \forall{b}.\;b$[Definition]
12. $\Gamma,\ t=\mathsf{F}\vdash \forall{b}.\;b$[SUBST 11,10]
13. $\Gamma,\ t=\mathsf{F}\vdash t$[SPEC 12]
14. $\vdash \forall{b}.\; (b = \mathsf{T})\lor(b = \mathsf{F})$[Axiom]
15. $\vdash (t = \mathsf{T})\lor(t = \mathsf{F})$[SPEC 14]
16. $t=\mathsf{T}\vdash t=\mathsf{T}$[ASSUME]
17. $t=\mathsf{T}\vdash t$[EQT_ELIM 16]
18. $\Gamma\vdash t$[DISJ_CASES 15,17,13]

Rewriting

Included in the set of derived inferences provided in HOL is a group of rules with complex definitions that do a limited amount of “automatic” theorem-proving in the form of rewriting. The ideas and implementation were originally developed by Milner and Wadsworth for Edinburgh LCF, and were later implemented more flexibly and efficiently by Paulson and Huet for Cambridge LCF. One basic rewriting rule, REWRITE_RULE, is illustrated here. Although HOL proofs typically feature more elaborate rewriters (such as SIMP_RULE, documented in Section), the underlying ideas are the same.

REWRITE_RULE uses a list of equational theorems (theorems whose conclusions can be regarded as having the form $t_1 = t_2$) to replace any subterms of an object theorem that “match” $t_1$ by the corresponding instance of $t_2$. The rule matches recursively and to any depth, until no more replacements can be made, using internally defined search, matching and instantiation algorithms. The validity of REWRITE_RULE rests ultimately on the primitive rules SUBST (for making the substitutions); INST_TYPE (for instantiating types); and the derived rules for generalization and specialization (see Section and Section for instantiating terms. The definition of REWRITE_RULE in ML also relies on a large number of general and HOL-oriented SML functions.

In practice, derived rules like REWRITE_RULE can play a central role in proofs, because they can perform a very large number of inferences which may happen in a complex and unpredictable order. This power is increased by the fact that any existing equational theorem can be supplied as a `rewrite rule', including a standard HOL set of pre-proved tautologies; and these rewrite rules can interact with each other in the rewriting process to transform the original theorem.

The application of REWRITE_RULE, in the session below, illustrates that replacements are made at all levels of the structure of a term. The example is a formula of natural number arithmetic: the infixes > and < are the usual “greater than” and “less than” relations, respectively, and SUC names the usual successor function on natural numbers. Use is made of the pre-existing definition of >, bound to arithmeticTheory.GREATER_DEF in SML (see §REFERENCE). The inference counting facility is used again, and the printing of theorems is adjusted as above.

> Count.inferences
    (REWRITE_RULE [arithmeticTheory.GREATER_DEF])
    (ASSUME ``SUC 3 > 0 /\ SUC 2 > 0 /\ SUC 1 > 0 /\ SUC 0 > 0``);
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 11; Total: 11
val it =  [.] ⊢ 0 < SUC 3 ∧ 0 < SUC 2 ∧ 0 < SUC 1 ∧ 0 < SUC 0:
   thm

Notice that rewriting equations can be extracted from universally quantified theorems. To construct the proof step-wise, with all of the instantiations, substitutions, uses of transitivity, etc, would be a lengthy process. The rewriting rules make it easy, and do so whilst still generating the entire chain of inferences.

Conversions

A conversion in HOL is a rule that maps a term to a theorem expressing the equality of that term to some other term. An example is the rule for $\beta$-conversion: $$ \mathtt{(}\backslash x\mathtt{.}\,t_1\mathtt{)}t_2 \quad \mapsto \quad \mathtt{|-}\,\mathtt{(}\backslash x\mathtt{.}\,t_1\mathtt{)}t_2\;\mathtt{=}\;t_1[t_2/x] $$

Theorems of this sort are used in HOL in a variety of contexts, to justify the replacement of particular terms by semantically equivalent terms.

The ML type of conversions is conv:

type conv = term -> thm

For example, BETA_CONV is an ML function of type conv (i.e., a conversion) that expresses $\beta$-conversion in HOL. It produces the appropriate equational theorem on $\beta$-redexes and fails elsewhere.

> BETA_CONV;
val it = fn: term -> thm

> BETA_CONV ``(\x. (\y. (\z. x + y + z)3)2) 1``;
val it =
   ⊢ (λx. (λy. (λz. x + y + z) 3) 2) 1 =
     (λy. (λz. 1 + y + z) 3) 2: thm

> BETA_CONV ``(\y. (\z. 1 + (y + z))3) 2``;
val it = ⊢ (λy. (λz. 1 + (y + z)) 3) 2 = (λz. 1 + (2 + z)) 3: thm

> BETA_CONV ``(\z. 1 + (2 + z)) 3``;
val it = ⊢ (λz. 1 + (2 + z)) 3 = 1 + (2 + 3): thm

> BETA_CONV ``1 + (2 + 3)``;
Exception- HOL_ERR (at Thm.BETA_CONV: not a beta-redex) raised

The basic conversions, as well as a number of those commonly used, are provided in HOL. There are also groups of application-specific conversions to be found in some of the libraries. (Of those provided, some are derived and some, like BETA_CONV, are taken as axiomatic.) In addition, HOL provides a collection of ML functions enabling users to define new conversions (as well as new rules and tactics) as functions of existing ones. The notion of conversions is inherited from Cambridge LCF; the underlying principles are described in (Paulson 1983, 1987).

Conversions such as BETA_CONV represent infinite families of equations. They are particularly useful in cases in which it is impossible to state, within the logic, a single axiom or theorem instantiable to every equation in a family. Instead, an ML procedure returns the instance of the desired theorem for any given term. This is also the reason that quite a few of the other rules in HOL are not stated instead as axioms or theorems. As rules, conversions are distinguished with an ML type abbreviation simply because there are relatively many of them with the same type, and because they return equational theorems that lend themselves directly to term rewriting. In many HOL applications, the main use of conversions is to produce these equational theorems. A few examples of conversions are illustrated below.

> NOT_FORALL_CONV “~!x. (f:'a->'a) x = g x”;
val it = ⊢ ¬(∀x. f x = g x) ⇔ ∃x. f x ≠ g x: thm

> CONTRAPOS_CONV ``(!x. f x = g x) ==> ((f:'a->'a) = g)``;
val it = ⊢ (∀x. f x = g x) ⇒ f = g ⇔ f ≠ g ⇒ ¬∀x. f x = g x: thm

> SELECT_CONV ``(@f:'a->'a. f x = g x)x = g x``;
val it = ⊢ (@f. f x = g x) x = g x ⇔ ∃f. f x = g x: thm

> EXISTS_UNIQUE_CONV “∃!z. (f:'a->'a) z = g z”;
val it =
   ⊢ (∃!z. f z = g z) ⇔
     (∃z. f z = g z) ∧ ∀z z'. f z = g z ∧ f z' = g z' ⇒ z = z':
   thm

An example of an application-specific conversion is numLib's num_CONV:

> numLib.num_CONV “2”;
val it = ⊢ 2 = SUC 1: thm

> numLib.num_CONV “1”;
val it = ⊢ 1 = SUC 0: thm

> numLib.num_CONV “0” handle e => Raise e;
Exception- 
Exception raised at Num_conv.num_CONV: Term either not a numeral or zero
HOL_ERR (at Num_conv.num_CONV: Term either not a numeral or zero) raised

Another application of conversions, related to the first, is in the implementation of the existing rewriting tools, REWRITE_CONV, REWRITE_RULE and REWRITE_TAC, which are central to theorem proving in HOL. This use is explained in §Rewriting Tools, both as an example and because users may have occasion to construct rewriting tools of their own design, by similar methods. The next section introduces the conversion-building tools in general.

Indicating Unchangedness

All conversions may raise the special exception Conv.UNCHANGED on an input term t, as a “short-hand” instead of returning the reflexive theorem |- t = t. This is done for efficiency reasons. All of the connectives described below in §Conversion Combining Operators handle this exception appropriately. The standard function QCONV is provided to automatically handle this exception in contexts where it would be inappropriate (typically where a conversion is being called to provide a theorem directly). QCONV's implementation is

   fun QCONV c t = c t handle UNCHANGED => REFL t

Conversion Combining Operators

A term $u$ is said to reduce to a term $v$ by a conversion $c$ if there exists a finite sequence of terms $t_1$, $t_2$, $\dots$, $t_n$ such that:

  1. $u = t_1$ and $v = t_n$;
  2. $c\ t_i$ evaluates to the theorem |- $t_i$=$t_{i+1}$ for $1 \leq i < n$;
  3. The evaluation of $c\ t_n$ fails.

The first session of this chapter illustrates the reduction of the term

   (\x. (\y. (\z. x + y + z)3)2)1

to 1 + (2 + 3) by the conversion BETA_CONV, in a reduction sequence of length four:

   (\x. (\y. (\z. x + (y + z))3)2)1
   (\y. (\z. 1 + (y + z))3)2
   (\z. 1 + (2 + z))3
   1 + (2 + 3)

That is, BETA_CONV applies to each term of the sequence, except the fourth and last, to give a theorem equating that term to the next term. Therefore, each term of the sequence, from the second on, can be extracted from the theorem for the previous term; namely, it is the right hand side of the conclusion. The whole reduction can therefore be accomplished by repeated application of BETA_CONV to the terms of the sequence as they are generated.

To transform BETA_CONV to achieve this effect, two operators on conversions are introduced. The first one, infixed, is THENC, which sequences conversions.

    THENC : conv * conv -> conv

If $c_1\ t_1$ evaluates to $\Gamma_1$|- $t_1$=$t_2$ and $c_2\ t_2$ evaluates to $\Gamma_2$|- $t_2$=$t_3$, then ($c_1$THENC$c_2$) $t_1$ evaluates to $\Gamma_1\cup\Gamma_2$|-$t_1$=$t_3$. If the evaluation of $c_1\ t_1$ or the evaluation of $c_2\ t_2$ fails, then so does the evaluation of $c_1$THENC$c_2$. THENC is justified by the transitivity of equality.

The second, also infixed, is ORELSEC; this applies a second conversion if the application of the first one fails.

    ORELSEC : conv * conv -> conv

($c_1$ORELSEC$c_2$) $t$ evaluates to $c_1\ t$ if that evaluation succeeds, and to $c_2\ t$ otherwise. (The failure to evaluate is detected via the ML failure construct.)

The functions THENC and ORELSEC are used to define the desired operator, REPEATC, which successively applies a conversion until it fails:

    REPEATC : conv -> conv

REPEATC $c$ is intuitively equivalent to:

   (c THENC c THENC ... THENC c THENC ...) ORELSEC ALL_CONV

It is defined recursively:

   fun REPEATC c t = ((c THENC (REPEATC c)) ORELSEC ALL_CONV) t

The current example term can thus be completely reduced by use of BETA_CONV transformed by the REPEATC operator:

> REPEATC BETA_CONV;
val it = fn: conv

> REPEATC BETA_CONV ``(\x. (\y. (\z. x + y + z)3)2)1``;
val it = ⊢ (λx. (λy. (λz. x + y + z) 3) 2) 1 = 1 + 2 + 3: thm

BETA_CONV applies to terms of a certain top level form only, namely to $\beta$-redexes, and fails on terms of any other form. In addition, no number of repetitions of BETA_CONV will $\beta$-reduce arbitrary $\beta$-redexes embedded in terms. For example, the term shown below fails even at the top level because it is not a $\beta$-redex:

> BETA_CONV ``(((\x.(\y.(\z. x + y + z))) 1) 2) 3``;
Exception- HOL_ERR (at Thm.BETA_CONV: not a beta-redex) raised

> is_abs ``(((\x.(\y.(\z. x + y + z))) 1) 2)``;
val it = false: bool

The $\beta$-redex (\w.w)3 is not affected in the third input of the session shown below, because of its position in the structure of the whole term. This is so even though the whole term is reduced, and the subterm at top level could be reduced:

> BETA_CONV ``(\z. x + y + z) 3``;
val it = ⊢ (λz. x + y + z) 3 = x + y + 3: thm

> BETA_CONV ``(\w. w) 3``;
val it = ⊢ (λw. w) 3 = 3: thm

> REPEATC BETA_CONV ``(\z. x + y + z) ((\w. w) 3)``;
val it = ⊢ (λz. x + y + z) ((λw. w) 3) = x + y + (λw. w) 3: thm

To produce, from a conversion $c$, a conversion that applies $c$ to every subterm of a term, the function DEPTH_CONV can be applied to $c$:

    DEPTH_CONV : conv -> conv

DEPTH_CONV $c$ is a conversion $$ t \quad\mapsto\quad \mathtt{|-}\,t\,\mathtt{=}\,t' $$ where $t'$ is obtained from $t$ by replacing every subterm $u$ by $v$ if $u$ reduces to $v$ by $c$. (Subterms for which $c\ u$ fails are left unchanged.) The definition leaves open the search strategy; in fact, DEPTH_CONV $c$ traverses a term “bottom up”, once, and left-to-right, repeatedly applying $c$ to each subterm until no longer applicable. This helps with the two problems thus far:

> DEPTH_CONV BETA_CONV ``(((\x. (\y. (\z. x + y + z))) 1) 2) 3``;
val it = ⊢ (λx y z. x + y + z) 1 2 3 = 1 + 2 + 3: thm

> DEPTH_CONV BETA_CONV ``(\z. x + y + z) ((\w. w) 3)``;
val it = ⊢ (λz. x + y + z) ((λw. w) 3) = x + y + 3: thm

It may happen, however, that the result of such a conversion still contains subterms that could themselves be reduced at top level. For example:

> val t = ``(\f. \x. f x) (\n. n + 1)``;
val t = “(λf x. f x) (λn. n + 1)”: term

> DEPTH_CONV BETA_CONV t;
val it = ⊢ (λf x. f x) (λn. n + 1) = (λx. (λn. n + 1) x): thm

The function TOP_DEPTH_CONV does more searching and reduction than DEPTH_CONV: it replaces every subterm $u$ by $v'$ if $u$ reduces to $v$ by $c$ and $v$ recursively reduces to $v'$ by TOP_DEPTH_CONV $c$.

    TOP_DEPTH_CONV : conv -> conv

Thus:

> TOP_DEPTH_CONV BETA_CONV t;
val it = ⊢ (λf x. f x) (λn. n + 1) = (λx. x + 1): thm

Finally, the simpler function ONCE_DEPTH_CONV is provided:

    ONCE_DEPTH_CONV : conv -> conv

ONCE_DEPTH_CONV $c\ t$ applies $c$ once to the first term (and only the first term) on which it succeeds in a top-down traversal:

> ONCE_DEPTH_CONV BETA_CONV t;
val it = ⊢ (λf x. f x) (λn. n + 1) = (λx. (λn. n + 1) x): thm

> ONCE_DEPTH_CONV BETA_CONV ``(\x. (\n. n + 1) x)``;
val it = ⊢ (λx. (λn. n + 1) x) = (λx. x + 1): thm

The equational theorems returned by conversions are not always useful in equational form. To make the results more useful for theorem proving, a conversion can be converted to a rule or a tactic, using the functions CONV_RULE or CONV_TAC, respectively.

    CONV_RULE : conv -> thm -> thm
    CONV_TAC : conv -> tactic

CONV_RULE $c$(|-$t$) returns |- $t'$, where $c\ t$ evaluates to the equation |- $t$=$t'$. CONV_TAC $c$ is a tactic that converts the conclusion of a goal using $c$. CONV_RULE is defined by:

   fun CONV_RULE c th = EQ_MP (c (concl th)) th

(The validation of CONV_TAC also uses EQ_MP.) For example, the built-in rule BETA_RULE reduces some of the $\beta$-redex subterms of a term.

    BETA_RULE : thm -> thm

It is defined by:

   val BETA_RULE = CONV_RULE (DEPTH_CONV BETA_CONV)

The search invoked by BETA_RULE is adequate for some purposes but not others; for example, the first use shown below but not the second:

> BETA_RULE (ASSUME ``(((\x. (\y. (\z. x + y + z))) 1) 2) 3 < 10``);
val it =  [.] ⊢ 1 + 2 + 3 < 10: thm

> val th = ASSUME ``NEXT = ^t``;
val th =  [.] ⊢ NEXT = (λf x. f x) (λn. n + 1): thm

> BETA_RULE th;
val it =  [.] ⊢ NEXT = (λx. (λn. n + 1) x): thm

> BETA_RULE (BETA_RULE th);
val it =  [.] ⊢ NEXT = (λx. x + 1): thm

A more powerful $\beta$-reduction rule that used the second search strategy could be defined as shown below (this is not built into HOL).

> val TOP_BETA_RULE = CONV_RULE (TOP_DEPTH_CONV BETA_CONV);
val TOP_BETA_RULE = fn: thm -> thm

> TOP_BETA_RULE th;
val it =  [.] ⊢ NEXT = (λx. x + 1): thm

TOP_DEPTH_CONV is the traversal strategy used by the HOL rewriting tools described in §Rewriting Tools.

Writing Compound Conversions

There are several other conversion operators in HOL, which, together with THENC, ORELSEC and REPEATC are available for building more complex conversions, as well as rules, tactics, and so on. These are described below; several are good illustrations themselves of how functions are built using conversions. The section culminates with the explanation of how DEPTH_CONV, TOP_DEPTH_CONV, and ONCE_DEPTH_CONV are built.

The conversion NO_CONV is an identity for ORELSEC, useful in building functions.

    NO_CONV : conv

NO_CONV $t$ always fails.

The function FIRST_CONV returns $c\ t$ for the first conversion $c$, in a list of conversions, for which the evaluation of $c\ t$ succeeds.

    FIRST_CONV : conv list -> conv

FIRST_CONV [$c_1$;$\dots$;$c_n$] is equivalent, intuitively, to:

   c1 ORELSEC c2 ORELSEC ... ORELSEC cn

It is defined by:

   fun FIRST_CONV [] tm = NO_CONV tm
     | FIRST_CONV (c :: rst) tm =
         c tm handle HOL_ERR _ => FIRST_CONV rst tm

The conversion ALL_CONV is an identity for THENC, useful in building functions.

    ALL_CONV : conv

ALL_CONV $t$ evaluates to |- $t$=$t$. It is defined as being identical to REFL.

The function EVERY_CONV applies a list of conversions in sequence.

    EVERY_CONV : conv list -> conv

EVERY_CONV [$c_1$;$\dots$;$c_n$] is equivalent, intuitively, to:

   c1 THENC c2 THENC ... THENC cn

It is defined by:

   fun EVERY_CONV cl t =
       List.foldr (op THENC) ALL_CONV cl t
         handle HOL_ERR _ => raise ERR "EVERY_CONV" ""

The operator CHANGED_CONV converts one conversion to another that fails on arguments that it cannot change.

    CHANGED_CONV : conv -> conv

If $c\ t$ evaluates to |- $t$=$t'$, then CHANGED_CONV $c\ t$ also evaluates to |- $t$=$t'$, unless $t$ and $t'$ are the same (up to $\alpha$-conversion), in which case it fails.

The operator TRY_CONV maps one conversion to another that always succeeds, by replacing failures with the identity conversion.

    TRY_CONV : conv -> conv

If $c\ t$ evaluates to |- $t$=$t'$, then TRY_CONV $c\ t$ also evaluates to |- $t$=$t'$. If $c\ t$ fails, then TRY_CONV $c\ t$ evaluates to |- $t$=$t$. TRY_CONV is implemented by:

   fun TRY_CONV c = c ORELSEC ALL_CONV

It is used in the implementation of TOP_DEPTH_CONV (given later).

There are a number of operators for applying conversions to the immediate subterms of a term. These use the ML functions:

    MK_COMB : thm * thm -> thm
    MK_ABS : thm -> thm

MK_COMB and MK_ABS implement the following derived rules: $$ \frac{\Gamma_1\,\mathtt{|-}\,u_1\mathtt{=}v_1 \qquad \Gamma_2\,\mathtt{|-}\,u_2\mathtt{=}v_2}{\Gamma_1\cup\Gamma_2\,\mathtt{|-}\,u_1\,u_2\,\mathtt{=}\,v_1\,v_2} \quad \mathtt{MK\_COMB} $$ $$ \frac{\Gamma\,\mathtt{|-\,!}x\mathtt{.}u\mathtt{=}v}{\Gamma\,\mathtt{|-\,(}\backslash x\mathtt{.}u\mathtt{)\,=\,(}\backslash x\mathtt{.}v\mathtt{)}} \quad \mathtt{MK\_ABS} $$

The function SUB_CONV applies a conversion to the immediate subterms of a term.

    SUB_CONV : conv -> conv

In particular:

  • SUB_CONV $c$ “$x$” $=$ |- $x$=$x$;
  • SUB_CONV $c$ “$u\;v$” $=$ |- $u\,v$=$u'\,v'$, if $c\,u$ $=$ |- $u$=$u'$ and $c\,v$ $=$ |- $v$=$v'$;
  • SUB_CONV $c$ \$x$.$u$” $=$ |- (\$x$.$u$) = (\$x$.$u'$), if $c\,u$ $=$ |- $u$=$u'$.

SUB_CONV is implemented in terms of MK_COMB and MK_ABS:

   fun SUB_CONV c t =
       if is_comb t then
          let val (rator, rand) = dest_comb t
          in MK_COMB (c rator, c rand) end
       else if is_abs t then
          let val (bv, body) = dest_abs t
              val bodyth = c body
          in MK_ABS (GEN bv bodyth) end
       else ALL_CONV t

SUB_CONV, too, is used in the definitions of DEPTH_CONV and TOP_DEPTH_CONV.

Three other useful conversion operators, also for applying conversions to the immediate subterms of a term, are as follows:

    RATOR_CONV : conv -> conv
    RAND_CONV : conv -> conv
    ABS_CONV : conv -> conv

RATOR_CONV $c$ converts the operator of an application using $c$; RAND_CONV $c$ converts the operand of an application; and ABS_CONV $c$ converts the body of an abstraction. Combinations of these are useful for applying conversions to particular subterms. These are implemented by:

   fun RATOR_CONV c t =
     let val (rator, rand) = dest_comb t
     in
       MK_COMB (c rator, REFL rand)
     end

   fun ABS_CONV c t =
     let val (bv, body) = dest_abs t
         val bodyth = c body
     in
       MK_ABS (GEN bv bodyth)
     end

The following is an example session illustrating these immediate subterm conversions (recalling that the expression $t_1$+$t_2$ actually parses as + $t_1\,t_2$).

> val t = ``(\x.x + 1) m + (\x. x + 2) n``;
val t = “(λx. x + 1) m + (λx. x + 2) n”: term

> RAND_CONV BETA_CONV t;
val it =
   ⊢ (λx. x + 1) m + (λx. x + 2) n = (λx. x + 1) m + (n + 2): thm

> RATOR_CONV (RAND_CONV BETA_CONV) t;
val it = ⊢ (λx. x + 1) m + (λx. x + 2) n = m + 1 + (λx. x + 2) n:
   thm

Finally, the definitions of DEPTH_CONV and TOP_DEPTH_CONV are given below.

   fun DEPTH_CONV c t =
    (SUB_CONV (DEPTH_CONV c) THENC (REPEATC c)) t

   fun TOP_DEPTH_CONV c t =
    (REPEATC c THENC
     (TRY_CONV
       (CHANGED_CONV (SUB_CONV (TOP_DEPTH_CONV c)) THENC
        TRY_CONV (c THENC TOP_DEPTH_CONV c)))) t

   fun ONCE_DEPTH_CONV c t =
    (c ORELSEC (SUB_CONV (ONCE_DEPTH_CONV c))) t

Note that the extra argument t is needed to stop these definitions looping (because ML is a call-by-value language). Note also that the actual definition of ONCE_DEPTH_CONV used in the system has been optimised to use failure to avoid rebuilding unchanged subterms.

Built-in Conversions

Many conversions are predefined in HOL; only those likely to be of general interest are listed here.

Generalized beta-reduction

The conversion:

    PAIRED_BETA_CONV : conv

does generalized beta-conversion of tupled lambda abstractions applied to tuples.

Given the term:

   “(\(x1, ... ,xn).t) (t1, ... ,tn)”

PAIRED_BETA_CONV proves that:

   |- (\(x1, ... ,xn). t[x1,...,xn]) (t1, ... ,tn)  =  t[t1, ... ,tn]

The conversion works for arbitrarily nested tuples. For example:

> PAIRED_BETA_CONV “(\((a,b),(c,d)). [a;b;c;d]) ((1,2),(3,4))”;
val it =
   ⊢ (λ((a,b),c,d). [a; b; c; d]) ((1,2),3,4) = [1; 2; 3; 4]: thm

Arithmetical conversions

The conversion:

    ADD_CONV : conv

does addition by formal proof. If $n$ and $m$ are numerals then ADD_CONV "$n$+$m$" returns the theorem |- $n$+$m$=$s$, where $s$ is the numeral denoting the sum of $n$ and $m$. For example:

> ADD_CONV ``1 + 2``;
val it = ⊢ 1 + 2 = 3: thm

> ADD_CONV ``0 + 1000``;
val it = ⊢ 0 + 1000 = 1000: thm

> ADD_CONV ``101 + 102``;
val it = ⊢ 101 + 102 = 203: thm

For more general arithmetic, the conversion REDUCE_CONV handles all of the operators in the theory of natural number arithmetic (see Section 5.3.1):

> REDUCE_CONV ``2 * 3``;
val it = ⊢ 2 * 3 = 6: thm

> REDUCE_CONV ``2 ** 3 + 101 MOD 6``;
val it = ⊢ 2³ + 101 MOD 6 = 13: thm

List processing conversions

There are two useful built-in conversions for lists:

    LENGTH_CONV : conv
    list_EQ_CONV : conv -> conv

LENGTH_CONV computes the length of a list. A call to:

   LENGTH_CONV ``LENGTH[t1;...;tn]``

generates the theorem:

   |- LENGTH [t1;...;tn] = n

The other conversion, list_EQ_CONV, proves or disproves the equality of two lists, given a conversion for deciding the equality of elements. A call to:

   list_EQ_CONV conv "[u1;...;un] = [v1;...;vm]"

returns |- ([u1;...;un] = [v1;...;vm]) = F if:

  1. ~(n=m), or
  2. conv proves |- ($u_i$=$v_i$) = F for any $1 \leq i \leq m$.

|- ([u1;...;un] = [v1;...;vm]) = T is returned if:

  1. (n=m) and ui is syntactically identical to vi for $1 \leq i \leq m$, or
  2. (n=m) and conv proves |- ($u_i$=$v_i$) = T for $1 \leq i \leq n$.

Skolemization

Two conversions are provided for a higher-order version of Skolemization (using existentially quantified function variables rather than first-order Skolem constants).

The conversion

    X_SKOLEM_CONV : term -> conv

takes a variable parameter, $f$ say, and proves:

   |- (!x1 ... xn. ?y. t[x1,...,xn,y])  =  (?f. !x1 ... xn. t[x1,...,xn,f x1...xn])

for any input term !$x_1\ \dots\ x_n$. ?$y$. $t[x_1,\dots,x_n,y]$. Note that when $n=0$, this is equivalent to alpha-conversion:

  |- (?y. t[y]) = (?f. t[f])

and that the conversion fails if there is already a free variable $f$ of the appropriate type in the input term. For example:

> X_SKOLEM_CONV “f:num->'a” “!n:num. ?x:'a. x = (f n)”;
Exception- HOL_ERR (at Conv.X_SKOLEM_CONV: `f` free in the input term) raised

will fail. The conversion SKOLEM_CONV is like X_SKOLEM_CONV, except that it uses a primed variant of the name of the existentially quantified variable as the name of the skolem function it introduces. For example:

> SKOLEM_CONV “!x. ?y. P x y”;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = ⊢ (∀x. ∃y. P x y) ⇔ ∃y. ∀x. P x (y x): thm

Quantifier movement conversions

A complete and systematically-named set of conversions for moving quantifiers inwards and outwards through the logical connectives ~, /\, \/, and ==> is provided. The naming scheme is based on the following atoms:

   <quant> := FORALL | EXISTS
   <conn>  := NOT | AND | OR | IMP
   [dir]   := LEFT | RIGHT          (optional)

The conversions for moving quantifiers inwards are called:

   <quant>_<conn>_CONV

where the quantifier <quant> is to be moved inwards through <conn>.

The conversions for moving quantifiers outwards are called:

   [dir]_<conn>_<quant>_CONV

where <quant> is to be moved outwards through <conn>, and the optional [dir] identifies which operand (left or right) contains the quantifier. The complete set is:

   NOT_FORALL_CONV    |- ~(!x.P) = ?x.~P
   NOT_EXISTS_CONV    |- ~(?x.P) = !x.~P
   EXISTS_NOT_CONV    |- (?x.~P) = ~!x.P
   FORALL_NOT_CONV    |- (!x.~P) = ~?x.P
   FORALL_AND_CONV         |- (!x. P /\ Q) = (!x.P) /\ (!x.Q)
   AND_FORALL_CONV         |- (!x.P) /\ (!x.Q) = (!x. P /\ Q)
   LEFT_AND_FORALL_CONV    |- (!x.P) /\ Q = (!x'. P[x'/x] /\ Q)
   RIGHT_AND_FORALL_CONV   |- P /\ (!x.Q) = (!x'. P /\ Q[x'/x])
   EXISTS_OR_CONV          |- (?x. P \/ Q) = (?x.P) \/ (?x.Q)
   OR_EXISTS_CONV          |- (?x.P) \/ (?x.Q) = (?x. P \/ Q)
   LEFT_OR_EXISTS_CONV     |- (?x.P) \/ Q = (?x'. P[x'/x] \/ Q)
   RIGHT_OR_EXISTS_CONV    |- P \/ (?x.Q) = (?x'. P \/ Q[x'/x])
   FORALL_OR_CONV
     |- (!x.P \/ Q) = P \/ !x.Q          [x not free in P]
     |- (!x.P \/ Q) = (!x.P) \/ Q        [x not free in Q]
     |- (!x.P \/ Q) = (!x.P) \/ (!x.Q)   [x not free in P or Q]
   OR_FORALL_CONV
     |- (!x.P) \/ (!x.Q) = (!x.P \/ Q)   [x not free in P or Q]
   LEFT_OR_FORALL_CONV    |- (!x.P) \/ Q = !x'. P[x'/x] \/ Q
   RIGHT_OR_FORALL_CONV   |- P \/ (!x.Q)  = !x'. P \/ Q[x'/x]
   EXISTS_AND_CONV
     |- (?x.P /\ Q) = P /\ ?x.Q          [x not free in P]
     |- (?x.P /\ Q) = (?x.P) /\ Q        [x not free in Q]
     |- (?x.P /\ Q) = (?x.P) /\ (?x.Q)   [x not free in P or Q]
   AND_EXISTS_CONV
     |- (?x.P) /\ (?x.Q) = (?x.P /\ Q)   [x not free in P or Q]
   LEFT_AND_EXISTS_CONV    |- (?x.P) /\ Q = ?x'. P[x'/x] /\ Q
   RIGHT_AND_EXISTS_CONV   |- P /\ (?x.Q)  = ?x'. P /\ Q[x'/x]
   FORALL_IMP_CONV
     |- (!x.P ==> Q) = P ==> !x.Q          [x not free in P]
     |- (!x.P ==> Q) = (?x.P) ==> Q        [x not free in Q]
     |- (!x.P ==> Q) = (?x.P) ==> (!x.Q)   [x not free in P or Q]
   LEFT_IMP_FORALL_CONV   |- (!x.P) ==> Q = !x'. P[x/'x] ==> Q
   RIGHT_IMP_FORALL_CONV  |- P ==> (!x.Q) = !x'. P ==> Q[x'/x]
   EXISTS_IMP_CONV
     |- (?x.P ==> Q) = P ==> ?x.Q          [x not free in P]
     |- (?x.P ==> Q) = (!x.P) ==> Q        [x not free in Q]
     |- (?x.P ==> Q) = (!x.P) ==> (?x.Q)   [x not free in P or Q]
   LEFT_IMP_EXISTS_CONV   |- (?x.P) ==> Q = !x'. P[x/'x] ==> Q
   RIGHT_IMP_EXISTS_CONV  |- P ==> (?x.Q) = ?x'. P ==> Q[x'/x]

Rewriting Tools

The rewriting tool REWRITE_RULE was introduced in Chapter 2. There are also rewriting conversions like REWRITE_CONV. All of the various rewriting tools provided in HOL are implemented by use of conversions. Certain new tools could also be built in a similar way.

The rewriting primitive in HOL is REWR_CONV:

    REWR_CONV : thm -> conv

REWR_CONV ($\Gamma$|-$u$=$v$) $t$ evaluates to a theorem $\Gamma$|-$t$=$t'$ if $t$ is an instance (by type and/or variable instantiation) of $u$ and $t'$ is the corresponding instance of $v$. The first argument to REWR_CONV can be quantified. Below is an illustration.

> REWR_CONV ADD1 ``SUC 0``;
val it = ⊢ SUC 0 = 0 + 1: thm

All subterms of $t$ can be rewritten according to an equation $th$ using $$ \mathtt{DEPTH\_CONV(REWR\_CONV}\;th\mathtt{)} $$ as shown below. The function "PRE" is the usual predecessor function.

> DEPTH_CONV (REWR_CONV ADD1) ``SUC (SUC 0) = PRE (SUC 2)``;
val it = ⊢ SUC (SUC 0) = PRE (SUC 2) ⇔ 0 + 1 + 1 = PRE (2 + 1):
   thm

In itself, this is not a very useful rewriting tool, but a collection of others have been developed for use in HOL. All of the rewriting tools are, in fact, logically derived, and are based on conversions similar to DEPTH_CONV. They have been optimized in various ways, so their implementation is in some cases rather complex and is not given here. The conversions, rules and tactics for rewriting all take a list of theorems to be used as rewrites. The theorems in the list need not be in simple equational form (e.g., a conjunction of equations is permissible); but are converted to equational form automatically (and internally). (For example, a conjunction of equations is split into its constituent conjuncts.) There are also a number of standard equations (representing common tautologies) held in the ML variable basic_rewrites, and these are used by some of the rewriting tools. All the built-in rewriting tools are listed below, for reference, beginning with the rules. (All are fully described in §REFERENCE.)

The prefix PURE_ indicates that the built-in equations in basic_rewrites are not used (i.e., only those given explicitly are used). The prefix ONCE_ indicates that the tool makes only one rewriting pass through the expression (this is useful to avoid divergence). It is based on ONCE_DEPTH_CONV, while the other tools traverse using TOP_DEPTH_CONV.

The rewriting conversions are:

   REWRITE_CONV                : thm list -> conv
   PURE_REWRITE_CONV           : thm list -> conv
   ONCE_REWRITE_CONV           : thm list -> conv
   PURE_ONCE_REWRITE_CONV      : thm list -> conv

The basic rewriting rules are:

   REWRITE_RULE                      : thm list -> thm -> thm
   PURE_REWRITE_RULE                 : thm list -> thm -> thm
   ONCE_REWRITE_RULE                 : thm list -> thm -> thm
   PURE_ONCE_REWRITE_RULE            : thm list -> thm -> thm

The prefix ASM_ indicates that the rule rewrites using the assumptions of the theorem as rewrites.

   ASM_REWRITE_RULE                  : thm list -> thm -> thm
   PURE_ASM_REWRITE_RULE             : thm list -> thm -> thm
   ONCE_ASM_REWRITE_RULE             : thm list -> thm -> thm
   PURE_ONCE_ASM_REWRITE_RULE        : thm list -> thm -> thm

The prefix FILTER_ indicates that the rule only rewrites with those assumptions of the theorem satisfying the predicate supplied.

   FILTER_ASM_REWRITE_RULE           : (term -> bool) -> thm list -> thm -> thm
   FILTER_PURE_ASM_REWRITE_RULE      : (term -> bool) -> thm list -> thm -> thm
   FILTER_ONCE_ASM_REWRITE_RULE      : (term -> bool) -> thm list -> thm -> thm
   FILTER_PURE_ONCE_ASM_REWRITE_RULE : (term -> bool) -> thm list -> thm -> thm

Tactics are introduced in Chapter 4, but are listed here for reference. The tactics corresponding to the above rules are the following:

   REWRITE_TAC                       : thm list -> tactic
   PURE_REWRITE_TAC                  : thm list -> tactic
   ONCE_REWRITE_TAC                  : thm list -> tactic
   PURE_ONCE_REWRITE_TAC             : thm list -> tactic

The prefix ASM_ indicates that the tactic rewrites using the assumptions of the goal as rewrites.

   ASM_REWRITE_TAC                   : thm list -> tactic
   PURE_ASM_REWRITE_TAC              : thm list -> tactic
   ONCE_ASM_REWRITE_TAC              : thm list -> tactic
   PURE_ONCE_ASM_REWRITE_TAC         : thm list -> tactic

The prefix FILTER_ indicates that the tactic only rewrites with those assumptions of the goal satisfying the predicate supplied.

   FILTER_ASM_REWRITE_TAC            : (term -> bool) -> thm list -> tactic
   FILTER_PURE_ASM_REWRITE_TAC       : (term -> bool) -> thm list -> tactic
   FILTER_ONCE_ASM_REWRITE_TAC       : (term -> bool) -> thm list -> tactic
   FILTER_PURE_ONCE_ASM_REWRITE_TAC  : (term -> bool) -> thm list -> tactic

Goal Directed Proof: Tactics and Tacticals

There are three primary devices that together make theorem proving practical in HOL. All three originate with Milner for Edinburgh LCF. The first is the theory as a record of (among other things) facts already proved and thence available as lemmas without having to be re-proved. The second, the subject of Chapter 2, is the derived rule of inference as a meta-language procedure that implements a broad pattern of inference, but that also, at each application, generates every primitive step of the proof. The third device is the tactic as a means of organizing the construction of proofs; and the use of tacticals for composing tactics.

Even with recourse to derived inference rules, it is still surprisingly awkward to work forward, to find a chain of theorems that culminates in a desired theorem. This is in part because chains have no structure, while “proof efforts” do. For instance, if within one sequence, two chains of steps are to be combined in the end by conjunction, then one chain must follow or be interspersed with the other in the overall sequence. It can also be difficult to direct the proof toward its object when starting from only hypotheses (if any), lemmas (if any), axioms, and theorems following from no hypotheses (e.g., by ASSUME or REFL). Likewise, it can be equally difficult to reconstruct the plan of the proof effort after the fact, from the linear sequence of theorems; the sequence is unhelpful as documentation.

The idea of goal directed proof is a simple one, well known in artificial intelligence: to organize the search as a tree, and to reverse the process and begin with the objective. The goal is then decomposed, successively if necessary, into what one hopes are more tractable subgoals, each decomposition accompanied by a plan for translating the solution of subgoals into a solution of the goal. The choice of decomposition is an explicit way of expressing a proof “strategy”.

Thus, for example, instead of the linear sequencing of two branches of the proof of the conjunction, each branch starting from scratch, the proof task is organized as a tree search, starting with a conjunctive goal and decomposing it into the two conjunct subgoals (undertaken in optional order), with the intention of conjoining the two solutions when and if found. The proof itself, as a sequence of steps, is the same however it is found; the difference is in the search, and in the preservation, if required, of the structured proof plan.

The representation of this idea in LCF was Milner's inspiration; the idea is similarly central to theorem proving in HOL. Although subgoaling theorem provers had already been built at the time, Milner's particular contribution was in formalizing the method for translating subgoals solutions to solutions of goals.

Tactics, Goals and Validations

A tactic is an ML function that when applied to a goal reduces it to (i) a list of (sub)goals, along with (ii) a validation function mapping a list of theorems to a theorem. The idea is that this function justifies the decomposition of the goal, and so it is also known as a justification. A goal is an ML value whose type is isomorphic to, but distinct from, the ML abstract type thm of theorems. That is, a goal is a list of terms (assumptions) paired with a term. These two components correspond, respectively, to the list of hypotheses and the conclusion of a theorem. The list of assumptions is a working record of facts that may be used in decomposing the goal.

The relation of theorems to goals is achievement: a theorem achieves a goal if the conclusion of the theorem is equal to the term part of the goal (up to $\alpha$-conversion), and if each hypothesis of the theorem is equal (up to $\alpha$-conversion, again) to some assumption of the goal. This definition assures that the theorem purporting to satisfy a goal does not depend on assumptions beyond the working assumptions of the goal.

A tactic is said to solve a goal if it reduces the goal to the empty set of subgoals. This depends, obviously, on there being at least one tactic that maps a goal to the empty subgoal list. The simplest tactic that does this is one that can recognize when a goal is achieved by an axiom or an existing theorem; in HOL, the function ACCEPT_TAC does this. ACCEPT_TAC takes a theorem $\mathit{th}$ and produces a tactic that maps a value of type thm to the empty list of subgoals. It justifies this “decomposition” by a validation function that maps the empty list of theorems to the theorem $\mathit{th}$. The use of this technical device, or other such tactics, ends the decomposition of subgoals, and allows the proof to be built up.

Unlike theorems, goals need not be defined as an abstract type; they are transparent and can be constructed freely. Thus, an ML type abbreviation is introduced for goals. The operations on goals are therefore just the ordinary pair selectors and constructor. Likewise, type abbreviations are introduced for validations and tactics. Conceptually, the following abbreviations are made in HOL:

   goal       = term list * term
   tactic     = goal -> goal list * validation
   validation = thm list -> thm

It does not follow, of course, from the type tactic that a particular tactic is well-behaved. For example, suppose that $T\ g$ = ([$g_1$,$\dots$,$g_n$],$v$), and that the subgoals $g_1$, $\dots$, $g_n$ have been solved. That means that some theorems $\mathit{th}_1$, $\dots$, $\mathit{th}_n$ have been proved such that each $\mathit{th}_i$ ($1 \leq i \leq n$) achieves the goal $g_i$. The validation $v$ is intended to be a function that when applied to the list [$\mathit{th}_1$,$\dots$,$\mathit{th}_n$], succeeds in returning a theorem, $\mathit{th}$, achieving the original goal $g$; but, of course, it might sometimes not succeed. If $v$ succeeds for every list of achieving theorems, then the tactic $T$ is said to be valid. This does not guarantee, however, that the subgoals are solvable in the first place. If, in addition to being valid, a tactic always produces solvable subgoals from a solvable goal, it is called strongly valid.

Tactics can be perfectly useful without being strongly valid, or without even being valid; in fact, some of the most basic theorem proving strategies, expressed as tactics, are invalid or not strongly valid. An invalid tactic cannot result in the proof of false theorems; theorems in HOL are always the result of performing a proof in the basic logic, whether the proof is found by goal directed search or forward search. However, an invalid tactic may produce an unintended theorem — one that does not achieve the original goal. The typical case is when a theorem purporting to achieve a goal actually depends on hypotheses that extend beyond the assumptions of the goal. The inconvenience to the HOL user in this case is that the problem may be not immediately obvious; the default print format of theorems has hypotheses abbreviated as dots. Invalidity may also be the result of the failure of the proof function, in the ML sense of failure, when applied to a list of theorems (if, for example, the function were defined incorrectly); but again, no false theorems can result. Likewise, a tactic that is not strongly valid cannot result in a false theorem; the worst outcome of applying such a tactic is the production of unsolvable subgoals.

Tactics are specified using the following notation:

$$ \dfrac{\mathit{goal}} {\mathit{goal}_1\quad\mathit{goal}_2\quad\dots\quad\mathit{goal}_n} $$

For example, the tactic for decomposing conjunctions into two conjunct subgoals is called conj_tac. (The tactic is also available under the name CONJ_TAC.) It is described by:

$$ \dfrac{t_1\;\land\;t_2}{t_1\qquad t_2} $$

This indicates that conj_tac reduces a goal of the form ($\Gamma$,$t_1$/\$t_2$) to subgoals ($\Gamma$,$t_1$) and ($\Gamma$,$t_2$). The fact that the assumptions of the original goal are propagated unchanged to the two subgoals is indicated by the absence of assumptions in the notation. The notation gives no indication of the proof function.

Another example is numLib.INDUCT_TAC, a low-level tactic for performing mathematical induction on the natural numbers:

$$ \dfrac{\forall n.\;t[n]}{t[\mathtt{0}]\qquad \{t[n]\}\;t[\mathtt{SUC}\;n]} $$

Thus, INDUCT_TAC reduces a goal of the form ($\Gamma,\forall n.\;t[n]$) to a basis subgoal ($\Gamma$,$t[$0$]$) and an induction step subgoal ($\Gamma\cup\{t[n]\}$,$t[$SUC $n]$). The induction assumption is indicated in the tactic notation with set brackets.

Tactics fail (in the ML sense) if they are applied to inappropriate goals. For example, conj_tac will fail if it is applied to a goal whose conclusion is not a conjunction. Some tactics never fail; for example all_tac

$$ \dfrac{t}{t} $$

is the identity tactic; it reduces a goal ($\Gamma$,$t$) to the single subgoal ($\Gamma$,$t$)i.e., it has no effect. The all_tac tactic is useful for writing compound tactics, as discussed later (see §Tacticals).

In just the way that the derived rule REWRITE_RULE can be used in forward proof, the corresponding tactic rewrite_tac can be used in goal-directed proof. Given a goal and a list of equational theorems, rewrite_tac transforms the term component of the goal by applying the equations as left-to-right rewrites, recursively and to all depths, until no more changes can be made. Unless not required, the function includes as rewrites the same standard set of pre-proved tautologies that REWRITE_RULE uses. By use of the tautologies, some subgoals can be solved internally by rewriting, and in that case, an empty list of subgoals is returned. The transformation of the goal is justified in each case by the appropriate chain of inferences. Rewriting is a simple implementation of an extremely powerful idea. The more sophisticated simplification functions (e.g., simp) often do a very large share of the work in goal directed proof searches.

A simple example from list theory illustrates the use of tactics. A conjunctive goal is declared, and conj_tac applied to it:

> val gl0 = ([]:term list,``(HD[1;2;3] = 1) /\ (TL[1;2;3] = [2;3])``);
val gl0 = ([], “HD [1; 2; 3] = 1 ∧ TL [1; 2; 3] = [2; 3]”):
   term list * term

> val (gl1,p1) = conj_tac gl0;
val gl1 =
   [([], “HD [1; 2; 3] = 1”), ([], “TL [1; 2; 3] = [2; 3]”)]:
   goal list
val p1 = fn: validation

The subgoals are each rewritten, using the definitions of HD and TL:

> open listTheory;   ... output elided ...

> HD;
val it = ⊢ ∀h t. HD (h::t) = h: thm

> TL;
val it = ⊢ ∀h t. TL (h::t) = t: thm

> val (gl1_1,p1_1) = rewrite_tac[HD,TL](hd gl1);
val gl1_1 = []: goal list
val p1_1 = fn: validation

> val (gl1_2,p1_2) = rewrite_tac[HD,TL](hd(tl gl1));
val gl1_2 = []: goal list
val p1_2 = fn: validation

Both of the two subgoals are now solved, so the decomposition is complete and the proof can be built up in stages. First the theorems achieving the subgoals are proved, then from those, the theorem achieving the original goal:

> val th1 = p1_1[];
val th1 = ⊢ HD [1; 2; 3] = 1: thm

> val th2 = p1_2[];
val th2 = ⊢ TL [1; 2; 3] = [2; 3]: thm

> p1[th1,th2];
val it = ⊢ HD [1; 2; 3] = 1 ∧ TL [1; 2; 3] = [2; 3]: thm

Although only the theorems achieving the subgoals are “seen” here, the proof functions of the three tactic applications together perform the entire chain of inferences leading to the theorem achieving the goal. The same proof could be constructed by forward search, starting from the definitions of HD and TL, but not nearly as easily.

The HOL system provides a very large collection of pre-defined tactics that includes conj_tac, INDUCT_TAC, all_tac and rewrite_tac. The pre-defined tactics are adequate for many applications. In addition, there are two means of defining new tactics. Since a tactic is an ML function, the user can define a new tactic directly in ML. Definitions of this sort use ML functions to construct the term part of the subgoals from the term part of the original goal (if any transformation is required); and they specify the justification, which expects a list of theorems achieving the subgoals and returns the theorem achieving (one hopes) the goal. The proof of the theorem is encoded in the definition of the justification function; that is, the means for deriving the desired theorem from the theorems given. This typically involves references to axioms and primitive and defined inference rules, and is usually the more difficult part of the project.

A simple example of a tactic written in ML is afforded by conj_tac, whose definition in HOL is as follows:

   fun conj_tac (asl,w) =
     let val (l,r) = dest_conj w
     in
         ([(asl,l), (asl,r)], (fn [th1, th2] => CONJ th1 th2))
     end

This shows how the subgoals are constructed, and how the proof function is specified in terms of the derived rule CONJ.

The second method is to compose existing tactics by the use of ML functions called tacticals. The tacticals provided in HOL are listed in §Tacticals. For example, two existing tactics can be sequenced by use of the tactical >> (the function can also be written as THEN or as \\): if $T_1$ and $T_2$ are tactics, then the ML expression $T_1$ >> $T_2$ evaluates to a tactic that first applies $T_1$ to a goal and then applies $T_2$ to each subgoal produced by $T_1$. The tactical >> is an infixed ML function. Complex and powerful tactics can be constructed in this way; and new tacticals can also be defined, although this is unusual.

The example from earlier is continued, to illustrate the use of the tactical >>:

> val (gl2,p2) = (conj_tac >> rewrite_tac [HD, TL]) gl0;
val gl2 = []: goal list
val p2 = fn: thm list -> thm

> p2 [];
val it = ⊢ HD [1; 2; 3] = 1 ∧ TL [1; 2; 3] = [2; 3]: thm

The single tactic conj_tac >> rewrite_tac[HD;TL] solves the goal in one single application. The chain of inference computed, however, is exactly the same as in the interactive proof; only the search is different.

In general, the second method is both easier and more reliable. It is easier because it does not involve writing ML procedures (usually rather complicated procedures); and more reliable because the composed tactics are valid when the constituent tactics are valid, as a consequence of the way the tacticals are defined. Tactics written directly in ML may fail in a variety of ways, and although, as usual, they cannot cause false theorems to appear, the failures can be difficult to understand and trace. On the other hand, there are some proof strategies that cannot be implemented as compositions of existing tactics, and these have to be implemented directly in ML. Certain sorts of inductions are an example of this; as well as tactics to support some personal styles of proof.

Either sort of tactic can be difficult to apply by hand, as shown in the examples above. There can be a lot of book-keeping required to support such an activity. For this reason, most interactive theorem-proving uses the subgoal or goalstack package.

Some Tactics Built into HOL

This section contains a selection of the more commonly used tactics in the HOL system. (See §REFERENCE for the complete list, with fuller explanations.)

It should be recalled that the ML type thm_tactic abbreviates thm->tactic, and the type conv abbreviates term->thm.

Specialization

    gen_tac : tactic

Summary: Specializes a universally quantified theorem to an arbitrary value. Also available under the name GEN_TAC.

$$ \dfrac{\forall x.\,t[x]}{t[x']} $$

where $x'$ is a variant of $x$ not free in either goal or assumptions.

Use: Solving universally quantified goals. gen_tac is often the first step of a goal directed proof. strip_tac (see below) applies gen_tac to universally quantified goals.

Conjunction

    conj_tac : tactic

Summary: Splits a goal $t_1\land t_2$ into two subgoals, $t_1$ and $t_2$.

$$ \dfrac{t_1\;\land\;t_2}{t_1\qquad t_2} $$

Use: Solving conjunctive goals. conj_tac is also invoked by strip_tac (see below).

Combined simple decompositions

    strip_tac : tactic

Summary: Breaks a goal apart. The tactic strip_tac removes one outer connective from the goal, using conj_tac, gen_tac, and other tactics. If the goal has the form $t_1\land \cdots \land t_n$ ==> $t$ then strip_tac makes each $t_i$ into a separate assumption.

Use: Useful for splitting a goal up into manageable pieces. Often the best thing to do first is rpt strip_tac, where rpt is the tactical that repeatedly applies a tactic until it fails (see §Repetition).

Case analysis

    Cases_on : term quotation -> tactic

Summary: Cases_on $q$, where $q$ is a quotation denoting a term of an algebraic type (e.g., booleans, lists, natural numbers, types defined by the user with Datatype), does case analysis on that term. Let the relevant type have $m$ constructors $\mathsf{C}_1$ to $\mathsf{C}_m$, where $\mathsf{C}_i$ has $n_i$ arguments. If the term is a variable, then the variable is substituted out of the goal entirely:

$$ \dfrac{t}{t[q := \mathsf{C}_1\,a_{11}\cdots a_{1n_1}]\quad\cdots\quad t[q := \mathsf{C}_m\,a_{m1}\cdots a_{mn_m}]} $$

If the term denoted by $q$ is not simply a variable (e.g., Cases_on `n + 1` ), then fresh assumptions are introduced:

$$ \dfrac{t}{\{q = \mathsf{C}_1\,a_{11}\cdots a_{1n_1}\}t\quad\cdots\quad\{q = \mathsf{C}_m\,a_{m1}\cdots a_{mn_m}\}t} $$

Use: Case analysis.

Rewriting

    rewrite_tac : thm list -> tactic

Summary: rewrite_tac[$\mathit{th}_1$,$\dots$,$\mathit{th}_n$] transforms the term part of a goal by rewriting it with the given theorems $\mathit{th}_1$, $\dots$, $\mathit{th}_n$, and the set of pre-proved standard tautologies. Also written REWRITE_TAC.

$$ \dfrac{\{t_1, \ldots, t_m\}t}{\{t_1, \ldots, t_m\}t'} $$

where $t'$ is obtained from $t$ as described.

Use: Advancing goals by using definitions and previously proved theorems.

Other rewriting tactics (based on rewrite_tac) are:

  1. asm_rewrite_tac adds the assumptions of the goal to the list of theorems used for rewriting. Also written ASM_REWRITE_TAC.

PURE_ASM_REWRITE_TAC is like asm_rewrite_tac, but it doesn't use any built-in rewrites.

PURE_REWRITE_TAC uses neither the assumptions nor the built-in rewrites.

FILTER_ASM_REWRITE_TAC $p$ [$\mathit{th}_1$,$\dots$,$\mathit{th}_n$] simplifies the goal by rewriting it with the explicitly given theorems $\mathit{th}_1$, $\dots$, $\mathit{th}_n$, together with those assumptions of the goal which satisfy the predicate $p$ and also the standard rewrites.

See also the more powerful simplification tactics described in the simplifier chapter.

Resolution by Modus Ponens

    imp_res_tac : thm -> tactic

Summary: imp_res_tac $\mathit{th}$ does a limited amount of automated theorem proving in the form of forward inference; it “resolves” the theorem $\mathit{th}$ with the assumptions of the goal and adds any new results to the assumptions. The specification for imp_res_tac is:

$$ \dfrac{\{t_1,\ldots,t_m\}t}{\{t_1,\ldots,t_m,u_1,\ldots,u_n\}t} $$

where $u_1$, $\dots$, $u_n$ are derived by “resolving” the theorem $\mathit{th}$ with the existing assumptions $t_1$, $\dots$, $t_m$. Resolution in HOL is not classical resolution, but just Modus Ponens with one-way pattern matching (not unification) and term and type instantiation. The general case is where $\mathit{th}$ is of the canonical form

$$ \vdash \forall x_1\ldots x_p.\;v_1\Rightarrow v_2\Rightarrow\ldots\Rightarrow v_q\Rightarrow v $$

imp_res_tac $\mathit{th}$ then tries to specialize $x_1$, $\dots$, $x_p$ in succession so that $v_1$, $\dots$, $v_q$ match members of $\{t_1,\ldots,t_m\}$. Each time a match is found for some antecedent $v_i$, for $i$ successively equal to $1$, $2$, $\dots$, $q$, a term and type instantiation is made and the rule of Modus Ponens is applied. If all the antecedents $v_i$ (for $1 \leq i \leq q$) can be dismissed in this way, then the appropriate instance of $v$ is added to the assumptions. Otherwise, if only some initial sequence $v_1$, $\dots$, $v_k$ (for some $k$ where $1 < k < q$) of the assumptions can be dismissed, then the remaining implication:

$$ \vdash v_{k+1}\Rightarrow\ldots\Rightarrow v_q\Rightarrow v $$

is added to the assumptions.

For a more detailed description of resolution and imp_res_tac, see §REFERENCE.

Use: Deriving new results from a previously proved implicative theorem, in combination with the current assumptions, so that subsequent tactics can use these new results.

Identity

    all_tac : tactic

Summary: The identity tactic for the tactical >> (see the example in §Tactics, Goals and Validations, and §Sequencing). Useful for writing tactics.

Use:

Writing tacticals (see description of rpt in §Repetition).

With THENL (see §Selective sequencing); for example, if tactic $T$ produces two subgoals and $T_1$ is to be applied to the first while nothing is to be done to the second, then $T$ THENL $[T_1,$ all_tac$]$ is the tactic required.

Splitting logical equivalences

    eq_tac : tactic

Summary: eq_tac splits an equational goal into two implications (the “if-case” and the “only-if” case):

$$ \dfrac{u \Leftrightarrow v}{u\Rightarrow v\qquad v\Rightarrow u} $$

Use: Proving logical equivalences, i.e., goals of the form "$u \Leftrightarrow v$" where $u$ and $v$ are boolean terms. Note that the same end can often be achieved by rewriting with the theorem EQ_IMP_THM, which states $$ \vdash (u \Leftrightarrow v) \Leftrightarrow (u \Rightarrow v) \land (v \Rightarrow u) $$

Solving existential goals

    EXISTS_TAC : term -> tactic

Summary: EXISTS_TAC $u$ reduces an existential goal $\exists x.\;t[x]$ to the subgoal $t[u]$. (In ASCII form, the existential is printed as a question mark.)

$$ \dfrac{\exists x.\;t[x]}{t[u]} $$

Use: Proving existential goals.

Tacticals

A tactical is not represented by a single ML type, but is in general an ML function that returns a tactic (or tactics) as result. Tacticals may take parameters, and this is reflected in the variety of ML types that the built-in tacticals have. Tacticals are used for building compound tactics. Some important tacticals in the HOL system are listed below. For a complete list of the tacticals in HOL see §REFERENCE.

Alternation

    ORELSE : tactic * tactic -> tactic

The tactical ORELSE is an ML infix. If $T_1$ and $T_2$ are tactics, then the ML expression $T_1$ ORELSE $T_2$ evaluates to a tactic which applies $T_1$ unless that fails; if it fails, it applies $T_2$. ORELSE is defined in ML as a curried infix by

   (T1 ORELSE T2) g = T1 g handle HOL_ERR _ => T2 g

First success

    FIRST : tactic list -> tactic

The tactical FIRST applies the first tactic, in a list of tactics, that succeeds.

   FIRST [T1, T2, ..., Tn] = T1 ORELSE T2 ORELSE ... ORELSE Tn

Change detection

    CHANGED_TAC : tactic -> tactic

CHANGED_TAC $T$ $g$ fails if the subgoals produced by $T$ are just [$g$]; otherwise it is equivalent to $T\,g$. It is defined by the following, where

  set_eq: 'a list -> 'a list -> bool

tests whether two lists denote the same set (i.e., contain the same elements).

   fun CHANGED_TAC tac g =
    let val (gl,p) = tac g
    in
      if set_eq gl [g] then raise ERR "CHANGED_TAC" "no change"
      else (gl,p)
    end

Sequencing

>>   : tactic -> tactic -> tactic
   THEN : tactic -> tactic -> tactic
   \\   : tactic -> tactic -> tactic

The tactical >> is an ML infix. Its aliases THEN and \\ are also infixes. If $T_1$ and $T_2$ are tactics, then the ML expression $T_1\;$>>$\;T_2$ evaluates to a tactic which first applies $T_1$ and then applies $T_2$ to each subgoal produced by $T_1$.

Both >> and THEN associate to the left, which can lead to some counter-intuitive behaviours when they combine with other sequencing operators. However, chains of tactics connected with >> behave as one might expect. In particular, if $T_1$ produces $n$ sub-goals, and $T_2$ produces varying numbers of sub-goals when applied to each of those, then the expression $$ T_1 \texttt{ >> } T_2 \texttt{ >> } T_3 $$ will apply $T_3$ to all of the leaf sub-goals generated by the sequencing of $T_1$ and $T_2$.

Selective sequencing

   THENL : tactic -> tactic list -> tactic
   >|    : tactic -> tactic list -> tactic

If tactic $T$ produces $n$ subgoals and $T_1$, $\dots$, $T_n$ are tactics then $T$ >| [$T_1$,$\dots$,$T_n$] is a tactic which first applies $T$ and then applies $T_i$ to the $i$th subgoal produced by $T$. The tactical THENL is useful if one wants to apply different tactics to different subgoals.

Successive application

    EVERY : tactic list -> tactic

The tactical EVERY applies a list of tactics one after the other.

   EVERY [T1, T2, ..., Tn] = T1 >> T2 >> ... >> Tn

Repetition

    REPEAT : tactic -> tactic
    rpt : tactic -> tactic

If $T$ is a tactic then rpt $T$ is a tactic that repeatedly applies $T$ until it fails. It is defined in ML by:

   fun REPEAT T g = ((T THEN REPEAT T) ORELSE ALL_TAC) g

(The extra argument g is needed because ML does not use lazy evaluation.)

Tactics for Manipulating Assumptions

There are in general two kinds of tactics in HOL: those that transform the conclusion of a goal without affecting the assumptions, and those that do (also or only) affect the assumptions. The various tactics that rewrite are typical of the first class; those that do “resolution” belong to the second. Often, many of the steps of a proof in HOL are carried out “behind the scenes” on the assumptions, by tactics of the second sort. A tactic that in some way changes the assumptions must also have a justification that “knows how” to restore the corresponding hypotheses of the theorem achieving the subgoal. All of this is explicit, and can be examined by a user moving about the subgoal-proof tree. Using these tactics in the most straightforward way, the assumptions at any point in a goal-directed proof, i.e., at any node in the subgoal tree, form an unordered record of every assumption made, but not yet dismissed, up to that point.

In practice, the straightforward use of assumption-changing tactics, with the tools currently provided in HOL, presents at least two difficulties. The first is that assumption sets can grow to an unwieldy size, the number and/or length of terms making them difficult to read. In addition, forward-search tactics such as resolution often add at least some assumptions that are never subsequently used, and these have to be carried along with the useful assumptions; the straightforward method provides no ready way of intercepting their arrival. Likewise, there is no straightforward way of discarding assumptions after they have been used and are merely adding to the clutter. Although perhaps against the straightforward spirit, this is a perfectly valid strategy, and requires no more than a way of denoting the specific assumptions to be discarded. That, however, raises the more general problem of denoting assumptions in the first place. Assumptions are also denoted so that they can be manipulated: given as parameters, combined to draw inferences, etc. The only straightforward way to denote them in the existing system is to supply their quoted text. Though adequate, this method may result in bulky ML expressions; and it may take some effort to present the text correctly (with necessary type information, etc.).

As always in HOL, there are quite a few ways around the various difficulties. One approach, of course, is the one intended in the original design of Edinburgh LCF, and advocates the rationale for providing a full programming language, ML, rather than a simple proof command set: that is for the user to implement new tactics in ML. For example, resolution tactics can be adapted by the user to add new assumptions more selectively; and case analysis tactics to make direct replacements without adding case assumptions. This, again, is adequate, but can involve the user in extensive amounts of programming, and in debugging exercises for which there is no system support.

Short of implementing new tactics, two other standard approaches are reflected in the current system. Both were originally developed for Cambridge LCF; both reflect fresh views of the assumptions; and both rely on tacticals that transform tactics. The two approaches are partly but not completely complementary.

The first approach, described in this section, implicitly regards the assumption set, already represented as a list, as a stack, with a pop operation, so that the assumption at the top of the stack can be (i) discarded and (ii) denoted without explicit quotation. (The corresponding push adds new assumptions at the head of the list.) The stack can be generalized to an array to allow for access to arbitrary assumptions.

The other approach, described in §Theorem continuations without popping, gives a way of intercepting and manipulating results without them necessarily being added as assumptions in the first place. The two approaches can be combined in HOL interactions.

Theorem continuations with popping

The first proof style, that of popping assumptions from the assumption “stack”, is illustrated using its main tool: the tactical POP_ASSUM.

    POP_ASSUM : thm_tactic -> tactic
    pop_assum : thm_tactic -> tactic

Given a function $f$:thm -> tactic, the tactic pop_assum $f$ applies $f$ to the (assumed) first assumption of a goal (i.e., to the top element of the assumption stack) and then applies the tactic created thereby to the original goal minus its top assumption:

   pop_assum f ([t1;...;tn], t) = f (ASSUME t1) ([t2;...;tn], t)

ML functions such as $f$, with type thm -> tactic, abbreviated to thm_tactic, are called theorem continuations, suggesting the fact that they take theorems and then continue the proof. The use of pop_assum can be illustrated by applying it to a particular tactic, namely DISCH_TAC.

    DISCH_TAC : tactic

On a goal whose conclusion is an implication $u \Rightarrow v$, DISCH_TAC reflects the natural strategy of attempting to prove $v$ under the assumption $u$, the discharged antecedent. For example, suppose it were required to prove that $(n = 0) \Rightarrow (n\times n = n)$:

> g `(n = 0) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ⇒ n * n = n

> e DISCH_TAC;
OK..
1 subgoal:
val it =
   
    0.  n = 0
   ------------------------------------
        n * n = n

Application of DISCH_TAC to the goal produces one subgoal, as shown, with the added assumption. To engage the assumption as a simple substitution, the tactic SUBST1_TAC is useful (see §REFERENCE for details).

    SUBST1_TAC : thm -> tactic

SUBST1_TAC expects a theorem with an equational conclusion, and substitutes accordingly, into the conclusion of the goal. At this point in the session, the tactical POP_ASSUM is applied to SUBST1_TAC to form a new tactic. The new tactic is applied to the current subgoal.

> p();
val it =
   
    0.  n = 0
   ------------------------------------
        n * n = n

> e(pop_assum SUBST1_TAC);
OK..
1 subgoal:
val it =
   
   0 * 0 = 0

The result, as shown, is that the assumption is used as a substitution rule and then discarded. The one subgoal therefore has no assumptions on its stack. The two tactics used thus far could be combined into one using the tactical >>:

> g `(n = 0) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ⇒ n * n = n

> e(DISCH_TAC >> pop_assum SUBST1_TAC);
OK..
1 subgoal:
val it =
   
   0 * 0 = 0

The goal can now be solved by simplification of arithmetic:

> e(simp[]);
OK..

Goal proved.
⊢ 0 * 0 = 0
val it =
   Initial goal proved.
   ⊢ n = 0 ⇒ n * n = n: proof

A single tactic can, of course, be written to solve the goal (indeed, the tactic simp[] solves the entire goal from the outset):

> restart();   ... output elided ...

> e(DISCH_TAC >> pop_assum SUBST1_TAC >> simp[]);
OK..
val it =
   Initial goal proved.
   ⊢ n = 0 ⇒ n * n = n: proof

This example illustrates how the tactical pop_assum provides access to the top of the assumption “stack” (a capability that is useful, obviously, only when the most recently pushed assumption is the very one required). To accomplish this access in the straightforward way would require some more awkward construct, with explicit assumptions:

> g `(n = 0) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ⇒ n * n = n

> e DISCH_TAC;
OK..
1 subgoal:
val it =
   
    0.  n = 0
   ------------------------------------
        n * n = n

> e(SUBST1_TAC(ASSUME ``n = 0``));
OK..
1 subgoal:
val it =
   
    0.  n = 0
   ------------------------------------
        0 * 0 = 0

In contrast to the above, the popping example also illustrates the convenient disappearance of an assumption no longer required, by removing it from the stack at the moment when it is accessed and used. This is valid because any theorem that achieves the subgoal will still achieve the original goal. Discarding assumptions is a separate issue from accessing them; there could, if one liked, be another tactical that produced a similar tactic on a theorem continuation to pop_assum but which did not pop the stack.

Finally, pop_assum $f$ induces case splits where $f$ does. To prove $(n=0 \lor n=1) \Rightarrow (n\times n = n)$, the function DISJ_CASES_TAC can be used. The tactic

DISJ_CASES_TAC |- $p$\/$q$

splits a goal into two subgoals that have $p$ and $q$, respectively, as new assumptions.

Simply using DISCH_TAC does not cause the disjunction to split when it becomes an assumption.

> g `((n = 0) \/ (n = 1)) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ∨ n = 1 ⇒ n * n = n

> e DISCH_TAC;
OK..
1 subgoal:
val it =
   
    0.  n = 0 ∨ n = 1
   ------------------------------------
        n * n = n

So, we can combine pop_assum and DISJ_CASES_TAC to apply the latter to the theorem corresponding to the disjunctive assumption:

> restart();   ... output elided ...

> e(DISCH_TAC >> pop_assum DISJ_CASES_TAC);
OK..
2 subgoals:
val it =
   
    0.  n = 1
   ------------------------------------
        n * n = n
   
    0.  n = 0
   ------------------------------------
        n * n = n

Indeed, we can then combine this with our earlier use of SUBST1_TAC to have both branches make progress at once:

> restart();   ... output elided ...

> e(DISCH_TAC >> pop_assum DISJ_CASES_TAC >> pop_assum SUBST1_TAC);
OK..
2 subgoals:
val it =
   
   1 * 1 = 1
   
   0 * 0 = 0

As noted earlier, pop_assum is useful when an assumption is required that is still at the top of the stack, as in the examples. However, it is often necessary to access assumptions made at arbitrary previous times, in order to give them as parameters, combine them, etc.

Two other useful tacticals that can be used to manipulate the assumption list are first_assum and qpat_assum. The first has an easy characterisation:

   first_assum f ([t1, ..., tn], t) =
    (f(ASSUME t1) ORELSE ... ORELSE f(ASSUME tn)) ([t1, ..., tn], t)

The first_x_assum tactical is similar to first_assum, but in addition to assuming one of the goal's assumptions as a theorem and passing this to the function $f$, the goal-state that $f\,(t_i\vdash t_i)$ acts upon has had $t_i$ removed from the assumption list. It is a “popping” version of first_assum.

The qpat_assum tactical takes a pattern-quotation as its first argument. The second thm_tactic parameter is then passed the first assumption that matches this pattern.

Thus:

   qpat_assum pat f ([t1, ..., tp, ..., tn], t) = f (ASSUME tp) ([t1, ..., tn], t)

where $t_p$ is the first assumption that matches the pattern pat.

Just as with first_assum, there is a “popping” version called qpat_x_assum that removes the matching assumption from the list. Thus:

   qpat_x_assum pat f ([t1, ..., tp, ..., tn], t) =
     f (ASSUME tp) ([t1, ..., tp-1, tp+1, ..., tn], t)

where $t_p$ is the first assumption that matches the pattern pat.

While first_assum (and first_x_assum) try to apply their theorem-tactic to every assumption, eventually using the first that succeeds, qpat_assum (and qpat_x_assum) only apply their theorem-tactic to one assumption (the first that matches).

Theorem continuations without popping

The idea of the second approach is suggested by the way the array-style tacticals supply a list of theorems (the assumed assumptions) to a function. These tacticals use the function to infer new results from the list of theorems, and then to do something with the results. In some cases, e.g., the last example, the assumptions need never have been made in the first place, which suggests a different use of tacticals. The original example for pop_assum illustrates this: namely, to show that $(n = 0) \Rightarrow (n\times n = n)$. Here, instead of discharging the antecedent by applying DISCH_TAC to the goal, which adds the antecedent as an assumption and returns the consequent as the conclusion, and then supplying the (assumed) added assumption to the theorem continuation SUBST1_TAC and discarding it at the same time, a tactical called disch_then is applied to SUBST1_TAC directly. disch_then transforms SUBST1_TAC into a new tactic: one that applies SUBST1_TAC directly to the (assumed) antecedent, and the resulting tactic to a subgoal with no new assumptions and the consequent as its conclusion:

> disch_then;
val it = fn: thm_tactic -> tactic

> disch_then SUBST1_TAC;
val it = fn: tactic

> g `(n = 0) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ⇒ n * n = n

> e(disch_then SUBST1_TAC);
OK..
1 subgoal:
val it =
   
   0 * 0 = 0

This gives the same result as the stack method, but more directly, with a more compact ML expression, and with the attractive feature that the term $n=0$ is never an assumption, even for an interval of one step. This technique is often used at the moment when results are available; as above, where the result produced by discharging the antecedent can be immediately passed to substitution. If the result were only needed later, it would have to be held as an assumption. However, results can be manipulated when they are available, and their results either held as assumptions or used immediately. For example, to prove $(0 = n) \Rightarrow (n \times n = n)$, the result $n=0$ could be reversed immediately:

> g `(0 = n) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        0 = n ⇒ n * n = n
> e(disch_then(SUBST1_TAC o SYM));
OK..
1 subgoal:
val it =
   
   0 * 0 = 0

The justification of disch_then SUBST1_TAC is easily constructed from the justification of DISCH_TAC composed with the justification of SUBST1_TAC. The term $n=0$ is assumed, to yield the theorem that is passed to the theorem continuation SUBST1_TAC, and it is accordingly discharged during the construction of the actual proof; but the assumption happens only internally to the tactic disch_then SUBST1_TAC, and not as a step in the tactical proof. In other words, the subgoal tree here has one node fewer than before, when an explicit step (DISCH_TAC) reflected the assumption.

On the goal with the disjunctive antecedent, this method again provides a compact tactic:

> g `((n = 0) \/ (n = 1)) ==> (n * n = n)`;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        n = 0 ∨ n = 1 ⇒ n * n = n

> e(disch_then(DISJ_CASES_THEN SUBST1_TAC));
OK..
2 subgoals:
val it =
   
   1 * 1 = 1
   
   0 * 0 = 0

This avoids the repeated popping and pushing of the stack solution, and likewise, gives a shorter ML expression. Both give a shorter expression than the direct method, which is:

   DISCH_TAC
    > DISJ_CASES_TAC(ASSUME ``(n = 0) \/ (n = 1)``)
    val it = fn: tactic
    >| [SUBST1_TAC(ASSUME ``n = 0``);
        SUBST1_TAC(ASSUME ``n = 1``)]

To summarize, there are so far at least five ways to solve a goal (and these are often combined in one interaction): directly, using the stack view of the assumptions, using the array view with or without discarding assumptions, and using a tactical to intercept an assumption step. All of the following work on the goal $(n=0) \Rightarrow (n \times n = n)$:

   DISCH_TAC
    > SUBST1_TAC(ASSUME ``n = 0``)
    val it = fn: tactic
    > simp[]
    val it = fn: tactic

   DISCH_TAC
    > pop_assum SUBST1_TAC
    val it = fn: tactic
    > simp[]
    val it = fn: tactic

   disch_then SUBST1_TAC
    > simp[]
    val it = fn: tactic

Furthermore, all induce the same sequence of inferences leading to the desired theorem; internally, no inference steps are saved by the economies in the ML text or the subgoal tree. In this sense, the choice is entirely one of style and taste; of how to organize the decomposition into subgoals. The first expression illustrates the verbosity of denoting assumptions by text (the goal with the disjunctive antecedent gave a clearer example); but also the intelligibility of the resulting expression, which, of course, is all that is saved of the interaction, aside from the final theorem. The last expression illustrates both the elegance and the inscrutibility of using functions to manipulate intermediate results directly, rather than as assumptions. The middle expression shows how results can be used as assumptions (discarded when redundant, if desired); and how assumptions can be denoted without recourse to their text.

It is a strength of the LCF approach to theorem proving that many different proof styles are supported, (all in a secure way) and indeed, can be studied in their own right.

HOL provides several other theorem continuation functions analogous to disch_then and DISJ_CASES_THEN. (Their names always end with _THEN, _THENL or _THEN2.) Some of these do convenient inferences for the user. For example:

    CHOOSE_THEN : thm_tactical

Where thm_tactical abbreviates thm_tactic -> tactic. CHOOSE_THEN $f$ ($\vdash\;\exists x.\;t[x]$) is a tactic that, given a goal, generates the subgoal obtained by applying $f$ to ($t[x]\vdash t[x]$). The intuition is that if |- $\exists x.\,t[x]$ holds then |- $t[x]$ holds for some value of $x$ (as long as the variable $x$ is not free elsewhere in the theorem or current goal). This gives an easy way of using existentially quantified theorems, something that is otherwise awkward.

The new method has other applications as well, including as an implementation technique. For example, taking DISJ_CASES_THEN as basic, DISJ_CASES_TAC can be defined by:

   val DISJ_CASES_TAC = DISJ_CASES_THEN ASSUME_TAC

Similarly, the method is useful for modifying existing tactics (e.g., resolution tactics) without having to re-program them in ML. This avoids the danger of introducing tactics whose justifications may fail, a particularly difficult problem to track down; it is also much easier than starting from scratch.

The main theorem continuation functions in the system are:

   ANTE_RES_THEN
   CHOOSE_THEN      X_CHOOSE_THEN
   CONJUNCTS_THEN   CONJUNCTS_THEN2
   DISJ_CASES_THEN  DISJ_CASES_THEN2   DISJ_CASES_THENL
   DISCH_THEN
   IMP_RES_THEN
   RES_THEN
   STRIP_THM_THEN
   STRIP_GOAL_THEN

See §REFERENCE for full details.

Advanced Tactics and Proof Techniques

Proof Suspension and Resumption

HOL's proof suspension and resumption facilities offer users the ability to structure large source files and proof effort by separating out sub-goals to be proved in separate Theorem-Proof-QED blocks. The first step of the process is to use the suspend tactic to “solve” a subgoal arising in the middle of a normal proof. This tactic has type:

    suspend : string -> bossLib.tactic

The suspend tactic's string argument is the label for the subgoal that is being suspended. When applied, this tactic immediately ends work on this subgoal as if it had been genuinely proved. Interactively, the goal stack will move on to any remaining subgoals in the proof. Theorems that have been “proved” with the help of this tactic are not stored/saved to the current segment (as they are incomplete), and instead a message is emitted indicating as much:

> Theorem to_suspend:
     p /\ q ==> q /\ p
  Proof
      strip_tac >> conj_tac
      >- suspend "rtp_q"
      >- simp[]
  QED
<<HOL message: Stashing suspended theorem to_suspend with pending subgoal: rtp_q.>>
val to_suspend =  [.] ⊢ p ∧ q ⇒ q ∧ p: thm

Here the user has decided to suspend the subgoal that has assumptions p and q (these are produced through the strip_tac application) and conclusion q (the first goal produced by conj_tac). A theorem is bound to the provided name in the SML namespace, but is not yet saved for export (it is incomplete). To resume work on the unproven subgoal, it is necessary to use the Resume form, specifying the theorem name first, and then providing the subgoal name as if it were a theorem/proof attribute. (See Chapter 9 for more on the syntax of attributes.)

Thus:

> Resume to_suspend[rtp_q,smlname=qsubgoal]:
    first_assum ACCEPT_TAC
  QED
val qsubgoal =  [q] ⊢ q: thm

The optional smlname attribute allows for inspection of the theorem corresponding to the sub-goal. To finish their work on the whole theorem, the user must now “finalise” it. This step checks that the theorem has indeed had all of its subgoals proved and saves it, possibly alongside the usual collection of attributes:

> Finalise to_suspend[simp]
val to_suspend =  [] ⊢ p ∧ q ⇒ q ∧ p: thm

If the subgoals associated with a name have not all been proved, the Finalise step will fail. Also, if a theorem has not been finalised by the end of the containing script-file, the theory-saving process will fail with an exception.

Niceties

  • If the suspend tactic is called multiple times with the same name within the same goal, resumption at that label will present the user with a complicated looking encoding of multiple goals within just one. In order to convert this one goal back into the multiple goals that were originally saved, the user should use the RESUME_TAC tactic.
  • Goals that have been resumed can again be suspend-ed, reusing subgoal names, or not.
  • Editor modes should make it easy to create appropriate goals when working interactively; this functionality is in turn making use of the underlying function
      set_suspended_goal :
        {label_name: string, suspension_name: string} -> proofs
    

Core Theories

The HOL system provides a collection of theories on which to base verification tools or further theory development. In the rest of this section, these theories are briefly described. The sections that follow provide an overview of the contents of each theory. For a complete list of all the axioms, definitions and theorems in HOL, see the online resources distributed with the system. In particular, the HTML file help/HOLindex.html is a good place to start browsing the available theories. For a graphical picture of the theory hierarchy, see help/theorygraph/theories.html.

In addition, core theories of higher mathematics are described in Chapter 6.

The Theory min

The starting theory of HOL is the theory min. In this theory, the type constant bool of booleans, the binary type operator $(\alpha,\beta)$fun of functions, and the type constant ind of individuals are declared. Building on these types, three primitive constants are declared: equality, implication, and a choice operator:

Equality
Equality (= : 'a -> 'a -> bool) is an infix operator.
Implication
Implication (==> : bool -> bool -> bool) is the material implication and is an infix operator that is right-associative, i.e., x ==> y ==> z parses to the same term as x ==> (y ==> z).
Choice
Equality and implication are standard predicate calculus notions, but choice is more exotic: if $t$ is a term having type $\sigma\to{}$bool, then @x.$\,t\,$x (or, equivalently, $@$\,t$) denotes some member of the set whose characteristic function is $t$. If the set is empty, then @x.$\,t\,$x denotes an arbitrary member of the set denoted by $\sigma$. The constant @ is a higher order version of Hilbert's $\hilbert$-operator; it is related to the constant $\iota$ in Church's formulation of higher order logic. For more details, see Church's original paper (Church 1940), Leisenring's book on Hilbert's $\hilbert$-symbol (Leisenring 1969), or Andrews' textbook on type theory (Andrews 1986).

No theorems or axioms are placed in theory min. The primitive rules of inference of HOL depend on the presence of min.

Basic Theories

The most basic theories in HOL provide support for a standard collection of types. The theory bool defines the basis of the HOL logic, including the boolean operations and quantifiers. On this platform, quite a bit of theorem-proving infrastructure can already be built. Further basic types are developed in the theory of pairs (prod), disjoint sums (sum), the one-element type (one), and the option type.

The theory bool

At start-up, the initial theory for users of the HOL system is called bool, which is constructed when the HOL system is built. The theory bool is an extension of the combination of the “conceptual” theories LOG and INIT, described in the LOGIC manual. Thus it contains the four axioms for higher order logic. These axioms, together with the rules of inference described in Section 1.5.1, constitute the core of the HOL logic. Because of the way the HOL system evolved from LCF1, the particular axiomatization of higher order logic it uses differs from the classical axiomatization due to Church (Church 1940). The biggest difference is that in Church's formulation type variables are in the meta-language, whereas in the HOL logic they are part of the object language.

The logical constants T (truth), F (falsity), ~ (negation), /\ (conjunction), \/ (disjunction), ! (universal quantification), ? (existential quantification), and ?! (unique existence quantifier) can all be defined in terms of equality, implication and choice. The definitions listed below are fairly standard; each one is preceded by its ML name. Later definitions sometimes build on earlier ones.

   T_DEF              |- T  = ((\x:bool. x) = (\x. x))

   FORALL_DEF         |- !  = \P:'a->bool. P = (\x. T)

   EXISTS_DEF         |- ?  = \P:'a->bool. P($@ P)

   AND_DEF            |- /\ = \t1 t2. !t. (t1 ==> t2 ==> t) ==> t

   OR_DEF             |- \/ = \t1 t2. !t. (t1 ==> t) ==> (t2 ==> t) ==> t

   F_DEF              |- F  = !t. t

   NOT_DEF            |- ~  = (\t. t ==> F)

   EXISTS_UNIQUE_DEF  |- ?! = (\P. $? P /\ (!x y. P x /\ P y ==> (x = y)))

There are four axioms in the theory bool; the first three are the following:

BOOL_CASES_AX
  ⊢ ∀t. (t ⇔ T) ∨ (t ⇔ F)

ETA_AX
  ⊢ ∀t. (λx. t x) = t

SELECT_AX
  ⊢ ∀P x. P x ⇒ P ($@ P)

The fourth and last axiom of the HOL logic is the Axiom of Infinity. Its statement is phrased in terms of the function properties ONE_ONE and ONTO. The definitions are:

ONE_ONE_DEF
  ⊢ ONE_ONE = (λf. ∀x1 x2. f x1 = f x2 ⇒ x1 = x2)
ONTO_DEF
  ⊢ ONTO = (λf. ∀y. ∃x. y = f x)

The Axiom of Infinity is

INFINITY_AX
  ⊢ ∃f. ONE_ONE f ∧ ¬ONTO f

This asserts that there exists a one-to-one map from ind to itself that is not onto. This implies that the type ind denotes an infinite set.

The three other axioms of the theory bool, the rules of inference in Section 1.5.1 and the Axiom of Infinity are, together, sufficient for developing all of standard mathematics. Thus, in principle, the user of the HOL system should never need to make a non-definitional theory. In practice, it is often very tempting to take the risk of introducing new axioms because deriving them from definitions can be tedious---proving that ‘axioms’ follow from definitions amounts to proving their consistency.

Further definitions. The theory bool also supplies the definitions of a number of useful constants.

LET_DEF
  ⊢ LET = (λf x. f x)
COND_DEF
  ⊢ COND =
     (λt t1 t2. @x. ((t ⇔ T) ⇒ x = t1) ∧ ((t ⇔ F) ⇒ x = t2))
IN_DEF
  ⊢ $IN = (λx f. f x)

The constant LET is used in representing terms containing local variable bindings (i.e., let-terms). For example, the concrete syntax let v = M in N is translated by the parser to the term LET (\v.N) M. For the full description of how let expressions are translated, see Section 5.2.3.2.

The constant COND is used to represent conditional expressions. The concrete syntax $\mathtt{if}\;t_1\;\mathtt{then}\;t_2\;\mathtt{else}\;t_3$ abbreviates the application COND $t_1$ $t_2$ $t_3$.

The constant IN (written as an infix) is the basis of the modelling of sets by their characteristic functions. The term $x$IN$P$ can be read as "$x$ is an element of the set $P$", or (more in line with its definition) as “the predicate $P$ is true of $x$”.

Finally, the polymorphic constant ARB$:\alpha$ denotes a fixed but arbitrary element. ARB is occasionally useful when attempting to deal with the issue of partiality.

Restricted quantifiers

The theory bool also defines constants that implement restricted quantification. This provides a means of simulating subtypes and dependent types with predicates. The most heavily used are restrictions of the existential and universal quantifiers:

RES_FORALL_DEF
  ⊢ RES_FORALL = (λp m. ∀x. x ∈ p ⇒ m x)
RES_EXISTS_DEF
  ⊢ RES_EXISTS = (λp m. ∃x. x ∈ p ∧ m x)

RES_ABSTRACT_DEF
  ⊢ (∀p m x. x ∈ p ⇒ RES_ABSTRACT p m x = m x) ∧
     ∀p m1 m2.
       (∀x. x ∈ p ⇒ m1 x = m2 x) ⇒
       RES_ABSTRACT p m1 = RES_ABSTRACT p m2

The definition of RES_ABSTRACT is a characterising formula, rather than a direct equation. There are two important properties

  • if $y$ is an element of $P$ then $(\lambda x :: P.\; M)\,y = M[y/x]$
  • if two restricted abstractions agree on all values over their (common) restricting set, then they are equal.

For completeness, restricted versions of unique existence and indefinite description are provided, although hardly used.

RES_EXISTS_UNIQUE_DEF
  ⊢ RES_EXISTS_UNIQUE =
     (λp m. (∃x::p. m x) ∧ ∀x y::p. m x ∧ m y ⇒ x = y)
RES_SELECT_DEF
  ⊢ RES_SELECT = (λp m. @x. x ∈ p ∧ m x)

The definition of RES_EXISTS_UNIQUE uses the restricted quantification syntax with the :: symbol, referring to the earlier definitions RES_EXISTS and RES_FORALL. The :: syntax is used with restricted quantifiers to allow arbitrary predicates to restrict binding variables. The HOL parser allows restricted quantification of all of a sequence of binding variables by putting the restriction at the end of the sequence, thus with a universal quantification:

$$ \forall x \, y \, z \, {\tt ::} \; P \, . \; Q(x,y,z) $$

Here the predicate $P$ restricts all of $x$, $y$ and $z$.

Derived syntactic forms

The HOL quotation parser can translate various standard logical notations into primitive terms. For example, if + has been declared an infix (as explained in Section 1.6), as it is when arithmeticTheory has been loaded, then x+1 is translated to $+ x 1 . The escape character $ suppresses the infix behaviour of + and prevents the quotation parser getting confused. In general, $ can be used to suppress any special syntactic behaviour a token (such as if, + or let) might have. This is illustrated in the table below, in which the terms in the column headed ML quotation are translated by the quotation parser to the corresponding terms in the column headed Primitive term. Conversely, the terms in the latter column are always printed in the form shown in the former one. The ML constructor expressions in the rightmost column evaluate to the same values (of type term) as the other quotations in the same row.

Table: Non-primitive terms

Kind of termML quotationPrimitive termConstructor expression
Negation~$t$$~ $t$mk_neg($t$)
Disjunction$t_1$\/$t_2$$\/ $t_1\,t_2$mk_disj($t_1$,$t_2$)
Conjunction$t_1$/\$t_2$$/\ $t_1\,t_2$mk_conj($t_1$,$t_2$)
Implication$t_1$==>$t_2$$==> $t_1\,t_2$mk_imp($t_1$,$t_2$)
Equality$t_1$=$t_2$$= $t_1\,t_2$mk_eq($t_1$,$t_2$)
$\forall$-quantification!$x.t$$!(\$x.t$)mk_forall($x$,$t$)
$\exists$-quantification?$x.t$$?(\$x.t$)mk_exists($x$,$t$)
$\hilbert$-term@$x.t$$@(\$x.t$)mk_select($x$,$t$)
Conditionalif $t$then$t_1$else$t_2$COND $t\,t_1\,t_2$mk_cond($t$,$t_1$,$t_2$)
let-expressionlet $x$=$t_1$in$t_2$LET(\$x.t_2$)$t_1$mk_let(mk_abs($x$,$t_2$),$t_1$)

There are constructors, destructors and indicators for all the obvious constructs. (Indicators, e.g., is_neg, return truth values indicating whether or not a term belongs to the syntax class in question.) In addition to the constructors listed in the table there are constructors, destructors, and indicators for pairs and lists, namely mk_pair, mk_cons and mk_list (see the REFERENCE manual). The constants COND and LET are explained in the section on The theory bool above. The constants \/, /\, ==> and = are examples of infixes and represent $\vee$, $\wedge$, $\Rightarrow$ and equality, respectively. If $c$ is declared to be an infix, then the HOL parser will translate $t_1\;c\;t_2$ to $\mathtt{\$}c;t_1;t_2$.

The constants !, ? and @ are examples of *binders* and represent $\forall$, $\exists$ and $\hilbert$, respectively. If $c$ is declared to be a binder, then the HOL parser will translate $c;x.t$ to the combination $\mathtt{$}c\,(\lambda x.\,t)$ (i.e., the application of the constant $c$ to the representation of the abstraction $\lambda x.\,t$).

Table: Syntactic abbreviations

Abbreviated termMeaningConstructor expression
$t\,t_1\cdots t_n$($\cdots$($t\,t_1$)$\cdots t_n$)list_mk_comb($t$,[$t_1$, $\ldots$ ,$t_n$])
\$x_1\cdots x_n$.$t$\$x_1$. $\cdots$ \$x_n$.$t$list_mk_abs([$x_1$, $\ldots$ ,$x_n$],$t$)
!$x_1\cdots x_n$.$t$!$x_1$. $\cdots$ !$x_n$.$t$list_mk_forall([$x_1$, $\ldots$ ,$x_n$],$t$)
?$x_1\cdots x_n$.$t$?$x_1$. $\cdots$ ?$x_n$.$t$list_mk_exists([$x_1$, $\ldots$ ,$x_n$],$t$)

There are also constructors list_mk_conj, list_mk_disj, list_mk_imp for conjunctions, disjunctions, and implications respectively. The corresponding destructor functions are called strip_comb etc.

Theorems

A large number of theorems involving the logical constants are pre-proved in the theory bool. The following theorems illustrate how higher order logic allows concise expression of theorems supporting quantifier movement.

 LEFT_AND_FORALL_THM  |- !P Q. (!x. P x) /\ Q = !x. P x /\ Q
 RIGHT_AND_FORALL_THM |- !P Q. P /\ (!x. Q x) = !x. P /\ Q x

 LEFT_EXISTS_AND_THM  |- !P Q. (?x. P x /\ Q) = (?x. P x) /\ Q
 RIGHT_EXISTS_AND_THM |- !P Q. (?x. P /\ Q x) = P /\ ?x. Q x

 LEFT_FORALL_IMP_THM  |- !P Q. (!x. P x ==> Q) = (?x. P x) ==> Q
 RIGHT_FORALL_IMP_THM |- !P Q. (!x. P ==> Q x) = P ==> !x. Q x

 LEFT_EXISTS_IMP_THM  |- !P Q. (?x. P x ==> Q) = (!x. P x) ==> Q
 RIGHT_EXISTS_IMP_THM |- !P Q. (?x. P ==> Q x) = P ==> ?x. Q x

 LEFT_FORALL_OR_THM   |- !Q P. (!x. P x \/ Q) = (!x. P x) \/ Q
 RIGHT_FORALL_OR_THM  |- !P Q. (!x. P \/ Q x) = P \/ !x. Q x

 LEFT_OR_EXISTS_THM   |- !P Q. (?x. P x) \/ Q = ?x. P x \/ Q
 RIGHT_OR_EXISTS_THM  |- !P Q. P \/ (?x. Q x) = ?x. P \/ Q x

 EXISTS_OR_THM        |- !P Q. (?x. P x \/ Q x) = (?x. P x) \/ ?x. Q x
 FORALL_AND_THM       |- !P Q. (!x. P x /\ Q x) = (!x. P x) /\ !x. Q x

 NOT_EXISTS_THM       |- !P. ~(?x. P x) = !x. ~P x
 NOT_FORALL_THM       |- !P. ~(!x. P x) = ?x. ~P x

 SKOLEM_THM           |- !P. (!x. ?y. P x y) = ?f. !x. P x (f x)

Also, a theorem justifying Skolemization (SKOLEM_THM) is proved. Many other theorems may be found in bool theory.

Combinators

The theory combin contains the definitions of function composition (infixed o), a reversed function application operator, function override (infixed =+), and the combinators S, K, I, W, and C.

o_DEF
  ⊢ ∀f g. f ∘ g = (λx. f (g x))
APP_DEF
  ⊢ ∀x f. (x :> f) = f x
UPDATE_def
  ⊢ ∀a b. (a =+ b) = (λf c. if a = c then b else f c)
K_DEF
  ⊢ K = (λx y. x)
S_DEF
  ⊢ S = (λf g x. f x (g x))
I_DEF
  ⊢ I = S K K
W_DEF
  ⊢ W = (λf x. f x x)
C_DEF
  ⊢ flip = (λf x y. f y x)

The following elementary properties are proved in the theory combin:

o_THM
  ⊢ ∀f g x. (f ∘ g) x = f (g x)
o_ASSOC
  ⊢ ∀f g h. f ∘ g ∘ h = (f ∘ g) ∘ h

UPDATE_EQ
  ⊢ ∀f a b c. f⦇a ↦ c; a ↦ b⦈ = f⦇a ↦ c⦈
UPDATE_COMMUTES
  ⊢ ∀f a b c d. a ≠ b ⇒ f⦇a ↦ c; b ↦ d⦈ = f⦇b ↦ d; a ↦ c⦈
APPLY_UPDATE_THM
  ⊢ ∀f a b c. f⦇a ↦ b⦈ c = if a = c then b else f c

K_THM
  ⊢ ∀x y. K x y = x
S_THM
  ⊢ ∀f g x. S f g x = f x (g x)
I_THM
  ⊢ ∀x. I x = x
W_THM
  ⊢ ∀f x. W f x = f x x
C_THM
  ⊢ ∀f x y. flip f x y = f y x

The above illustrates that there are two ways of writing function update terms. As per the definition above (UPDATE_def), the infix =+ takes a key $k$ and a value $v$, and returns a higher-order function, which when in turn is passed a function $f$, returns a version of that function that has been updated to return $v$ when applied to $k$, and is otherwise the same as $f$. The same effect can be achieved with the “substitution style” syntax: $f\llparenthesis k\mapsto v\rrparenthesis$. There is an ASCII form of this notation as well:

> ``(k2 =+ v2) ((k1 =+ v1) f)``;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = “f⦇k2 ↦ v2; k1 ↦ v1⦈”: term
> ``f (| k |-> v |)``;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = “f⦇k ↦ v⦈”: term

There are no theorems about :>; its use is as a convenient syntax for function applications. For example, chains of updates can lose some parentheses if written

   f :> (k1 =+ v1) :> (k2 =+ v2) :> (k3 =+ v3)

This presentation also makes the order in which functions are applied read from left-to-right.

Having the symbols o, S, K, I, W, and C as built-in constants is sometimes inconvenient because they are often wanted as mnemonic names for variables (e.g., S to range over sets and o to range over outputs).2 Variables with these names can be used in the current system if o, S, K, I, W, and C are first hidden (see Section 8.1.2.8). In fact, this happens so often with the constant C that the name C is “hidden” by default. Instead, it can be written in fully-qualified form, as combin$C, or with the alias flip, as can be seen above.

Pairs

The Cartesian product type operator prod is defined in the theory pair. Values of type ($\sigma_1$,$\sigma_2$)prod are ordered pairs whose first component has type $\sigma_1$ and whose second component has type $\sigma_2$. The HOL type parser converts type expressions of the form :$\sigma_1$#$\sigma_2$ into ($\sigma_1$,$\sigma_2$)prod, and the printer inverts this transformation. Pairs are constructed with an infixed comma symbol

   $, : 'a -> 'b -> 'a # 'b

so, for example, if $t_1$ and $t_2$ have types $\sigma_1$ and $\sigma_2$ respectively, then $t_1$,$t_2$ is a term with type $\sigma_1$#$\sigma_2$. Usually, pairs are written within brackets: ($t_1$,$t_2$). The comma symbol associates to the right, so that ($t_1$,$t_2$,$\ldots$,$t_n$) means ($t_1$,($t_2$,$\ldots$,$t_n$)).

Defining the product type. The type of Cartesian products is defined by representing a pair ($t_1$,$t_2$) by the function

   \a b. (a = t1) /\ (b = t2)

The representing type of $\sigma_1$#$\sigma_2$ is thus $\sigma_1$->$\sigma_2$->bool. It is easy to prove the following theorem.3

   |- ?p:'a->'b->bool. (\p. ?x y. p = \a b. (a = x) /\ (b = y)) p

The type operator prod is defined by invoking new_type_definition with this theorem which results in the definitional axiom prod_TY_DEF shown below being asserted in the theory pair.

prod_TY_DEF
  ⊢ ∃rep.
       TYPE_DEFINITION (λp. ∃x y. p = (λa b. a = x ∧ b = y)) rep

Next, the representation and abstraction functions REP_prod and ABS_prod for the new type are introduced, along with the following characterizing theorem, by use of the function define_new_type_bijections.

ABS_REP_prod
  ⊢ (∀a. ABS_prod (REP_prod a) = a) ∧
     ∀r. (λp. ∃x y. p = (λa b. a = x ∧ b = y)) r ⇔
         REP_prod (ABS_prod r) = r

Pairs and projections. The infix constructor ',' is then defined to be an application of the abstraction function. Subsequently, two crucial theorems are proved: PAIR_EQ asserts that equal pairs have equal components and ABS_PAIR_THM shows that every term having a product type can be decomposed into a pair of terms.

COMMA_DEF
  ⊢ ∀x y. (x,y) = ABS_prod (λa b. a = x ∧ b = y)
PAIR_EQ
  ⊢ (x,y) = (a,b) ⇔ x = a ∧ y = b
ABS_PAIR_THM
  ⊢ ∀x. ∃q r. x = (q,r)

By Skolemizing ABS_PAIR_THM and making constant specifications for FST and SND, the following theorems are proved.

PAIR
  ⊢ ∀x. (FST x,SND x) = x
FST
  ⊢ ∀x y. FST (x,y) = x
SND
  ⊢ ∀x y. SND (x,y) = y

Pairs and functions. In HOL, a function of type $\alpha\#\beta\to\gamma$ always has a counterpart of type $\alpha\to\beta\to\gamma$, and vice versa. This conversion is accomplished by the functions CURRY and UNCURRY. These functions are inverses.

CURRY_DEF
  ⊢ ∀f x y. CURRY f x y = f (x,y)
UNCURRY_DEF
  ⊢ ∀f x y. UNCURRY f (x,y) = f x y

CURRY_UNCURRY_THM
  ⊢ ∀f. CURRY (UNCURRY f) = f
UNCURRY_CURRY_THM
  ⊢ ∀f. UNCURRY (CURRY f) = f

Mapping functions over a pair. Functions $f:\alpha\to\gamma_1$ and $g:\beta\to\gamma_2$ can be applied component-wise (##, infix) over a pair of type $\alpha\#\beta$ to obtain a pair of type $\gamma_1\#\gamma_2$.

PAIR_MAP_THM
  ⊢ ∀f g x y. (f ## g) (x,y) = (f x,g y)

Binders and pairs. When doing proofs, statements involving tuples may take the form of a binding (quantification or $\lambda$-abstraction) of a variable with a product type. It may be convenient in subsequent reasoning steps to replace the variables with tuples of variables. The following theorems support this.

FORALL_PROD
  ⊢ (∀p. P p) ⇔ ∀p_1 p_2. P (p_1,p_2)
EXISTS_PROD
  ⊢ (∃p. P p) ⇔ ∃p_1 p_2. P (p_1,p_2)
LAMBDA_PROD
  ⊢ ∀P. (λp. P p) = (λ(p1,p2). P (p1,p2))

The theorem LAMBDA_PROD involves a paired abstraction, discussed in Section 5.2.3.1.

Wellfounded relations on pairs. Wellfoundedness, defined in Section 5.3.1.4, is a useful notion, especially for proving termination of recursive functions. For pairs, the lexicographic combination of relations (LEX, infix) may be defined by using paired abstractions. Then the theorem that lexicographic combination of wellfounded relations delivers a wellfounded relation is easy to prove.

LEX_DEF
  ⊢ ∀R1 R2. R1 LEX R2 = (λ(s,t) (u,v). R1 s u ∨ s = u ∧ R2 t v)
WF_LEX
  ⊢ ∀R Q. WF R ∧ WF Q ⇒ WF (R LEX Q)

Paired abstractions

It is notationally convenient to include pairing in the lambda notation, as a simple pattern-matching mechanism. The quotation parser will convert the term \($x_1$,$x_2$).$t$ to UNCURRY(\$x_1\,x_2$.$t$). The transformation is done recursively so that, for example,

   \(x1, x2, x3). t

is converted to

   UNCURRY (\x1. UNCURRY(\x2 x3. t))

More generally, the quotation parser repeatedly applies the transformation:

$$ \mathtt{\backslash(}v_1\mathtt{,}v_2\mathtt{).}t \quad\leadsto\quad \mathtt{UNCURRY(\backslash}v_1\mathtt{.\backslash}v_2\mathtt{.}t\mathtt{)} $$

until no more variable structures remain. For example:

$$ \begin{aligned} \mathtt{\backslash(}x\mathtt{,}y\mathtt{).}t &\;\leadsto\; \mathtt{UNCURRY(\backslash}x\,y\mathtt{.}t\mathtt{)}\\ \mathtt{\backslash(}x_1\mathtt{,}x_2\mathtt{,}\ldots\mathtt{,}x_n\mathtt{).}t &\;\leadsto\; \mathtt{UNCURRY(\backslash}x_1\mathtt{.\backslash(}x_2\mathtt{,}\ldots\mathtt{,}x_n\mathtt{).}t\mathtt{)}\\ \mathtt{\backslash((}x_1\mathtt{,}\ldots\mathtt{,}x_n\mathtt{),}y_1\mathtt{,}\ldots\mathtt{,}y_m\mathtt{).}t &\;\leadsto\; \mathtt{UNCURRY(\backslash(}x_1\mathtt{,}\ldots\mathtt{,}x_n\mathtt{).\backslash(}y_1\mathtt{,}\ldots\mathtt{,}y_m\mathtt{).}t\mathtt{)} \end{aligned} $$

As a result of this parser translation, a variable structure, such as (x,y) in \(x,y).x+y, is not a subterm of the abstraction in which it occurs; it disappears on parsing. This can lead to unexpected errors (accompanied by obscure error messages). For example, antiquoting a pair into the bound variable position of a lambda abstraction fails:

> ``\(x,y).x+y``;
val it = “λ(x,y). x + y”: term

> val p = Term `(x:num,y:num)`;
val p = “(x,y)”: term

> Lib.try Term `\^p.x+y` handle _ => T

Exception raised at Term.dest_var: not a var
val it = “T”: term

If $b$ is a binder, then `b(x_1,x_2).t` is parsed as `b(\(x_1,x_2).t)`, and hence transformed as above. For example, !(x,y). x > y parses to $!(UNCURRY(\x.\y. x > y)).

let-terms

The quotation parser accepts let-terms similar to those in ML. For example, the following terms are allowed:

   let x = 1 and y = 2 in x+y

   let f(x,y) = (x*x)+(y*y); a = 20*20; b = 50*49 in f(a,b)

let-terms are actually abbreviations for ordinary terms which are specially supported by the parser and pretty printer. The constant LET is defined (in the theory bool) by:

   LET = (\f x. f x)

and is used to encode let-terms in the logic. The parser repeatedly applies the transformations:

$$ \begin{aligned} \mathtt{let}\ f\,v_1\,\ldots\,v_n=t_1\ \mathtt{in}\ t_2 &\;\leadsto\; \mathtt{LET(\backslash} f\mathtt{.}t_2\mathtt{)(\backslash} v_1\,\ldots\,v_n\mathtt{.}t_1\mathtt{)}\\ \mathtt{let}\ (v_1,\ldots,v_n)=t_1\ \mathtt{in}\ t_2 &\;\leadsto\; \mathtt{LET(\backslash(}v_1\mathtt{,}\ldots\mathtt{,}v_n\mathtt{).}t_2\mathtt{)}t_1\\ \mathtt{let}\ v_1=t_1\,\mathtt{and}\,\ldots\,\mathtt{and}\,v_n=t_n\ \mathtt{in}\ t &\;\leadsto\; \mathtt{LET(\ldots(LET(LET(\backslash}v_1\ldots v_n\mathtt{.} t\mathtt{)}t_1\mathtt{)}t_2\mathtt{)\ldots)}t_n \end{aligned} $$

The underlying structure of the term can be seen by applying destructor operations. For example:

> Term `let x = 1; y = 2; in x+y`;
val it = “let x = 1; y = 2 in x + y”: term

> dest_comb it;
val it = (“LET (λx. (let y = 2 in x + y))”, “1”): term * term

> Term `let (x,y) = (1,2) in x+y`;
val it = “let (x,y) = (1,2) in x + y”: term

> dest_comb it;
val it = (“LET (λ(x,y). x + y)”, “(1,2)”): term * term

Readers are encouraged to convince themselves that the translations of let-terms represent the intuitive meaning suggested by the surface syntax.

Disjoint sums

The theory sum defines the binary disjoint union type operator sum. A type ($\sigma_1$,$\sigma_2$)sum denotes the disjoint union of types $\sigma_1$ and $\sigma_2$. The type operator sum can be defined, just as prod was, but the details are omitted here.4 The HOL parser converts :$\sigma_1$+$\sigma_2$`` `` into `` ``:($\sigma_1$,$\sigma_2$`)sum , and the printer inverts this.

The standard operations on sums are:

   INL  : 'a -> 'a + 'b
   INR  : 'b -> 'a + 'b
   ISL  : 'a + 'b -> bool
   ISR  : 'a + 'b -> bool
   OUTL : 'a + 'b -> 'a
   OUTR : 'a + 'b -> 'b

These are all defined as constants in the theory sum. The constants INL and INR inject into the left and right summands, respectively. The constants ISL and ISR test for membership of the left and right summands, respectively. The constants OUTL and OUTR project from a sum to the left and right summands, respectively.

The following theorem is proved in the theory sum. It provides a complete and abstract characterization of the disjoint sum type, and is used to justify the definition of functions over sums.

sum_Axiom
  ⊢ ∀f g. ∃h. (∀x. h (INL x) = f x) ∧ ∀y. h (INR y) = g y

Also provided are the following theorems having to do with the discriminator functions ISL and ISR:

ISL
  ⊢ (∀x. ISL (INL x) ⇔ T) ∧ ∀y. ISL (INR y) ⇔ F
ISR
  ⊢ (∀x. ISR (INR x) ⇔ T) ∧ ∀y. ISR (INL y) ⇔ F

ISL_OR_ISR
  ⊢ ∀x. ISL x ∨ ISR x

The sum theory also provides the following theorems relating the projection functions and the discriminators.

OUTL
  ⊢ ∀x. OUTL (INL x) = x
OUTR
  ⊢ ∀x. OUTR (INR x) = x

INL
  ⊢ ∀x. ISL x ⇒ INL (OUTL x) = x
INR
  ⊢ ∀x. ISR x ⇒ INR (OUTR x) = x

The sum type operator can be seen as functorial over its arguments and so has a “map” function, SUM_MAP, with definition and results showing its functoriality:

SUM_MAP_def
  ⊢ (∀f g a. SUM_MAP f g (INL a) = INL (f a)) ∧
     ∀f g b. SUM_MAP f g (INR b) = INR (g b)
SUM_MAP_I
  ⊢ SUM_MAP I I = I
SUM_MAP_o
  ⊢ SUM_MAP f g ∘ SUM_MAP h k = SUM_MAP (f ∘ h) (g ∘ k)

The one-element type

The theory one defines the type one which contains one element. The type is also abbreviated as unit, which is the name of the analogous type in ML, and this is the type's preferred printing form. The constant one denotes this one element, but, again by analogy with ML, the preferred parsing and printing form for this constant is ().5 The pre-proved theorems in the theory one are:

one_axiom
  ⊢ ∀(f :α -> unit) (g :α -> unit). f = g
one
  ⊢ ∀(v :unit). v = ()
one_Axiom
  ⊢ ∀(e :α). ∃!(fn :unit -> α). fn () = e

These three theorems are equivalent characterizations of the type with only one value. The theory one is typically used in constructing more elaborate types.

The itself type

The unary itself type operator (in boolTheory) provides a family of singleton types akin to one. Thus, for every type $\alpha$, `α itself` is a type containing just one value. This value's name is the_value, but the parser and pretty-printer are set up so that for the type `α itself`, the_value can be written as `(:α)` (the syntax includes the parentheses). For example, (:num) is the single value inhabiting the type num itself.

The point of the itself type is that if one defines a function with `α itself` as the domain, the function picks out just one value in its range, and so one can think of the function as being one from the type to a value for the whole type.

For example, one could define

   finite_univ (:'a) = FINITE (UNIV :'a set)

It would then be straightforward to prove the following theorems

   ⊢ finite_univ(:bool)
   ⊢ ¬finite_univ(:num)
   ⊢ finite_univ(:'a) ∧ finite_univ(:'b) ⇒ finite_univ(:'a # 'b)

The itself type is used in the Finite Cartesian Product construction that underlies the fixed-width word type (see Section 5.3.8 below).

The option type

The theory option defines a type operator option that ‘lifts’ its argument type, creating a type with all of the values of the argument and one other, specially distinguished value. The constructors of this type are

   NONE : 'a option
   SOME : 'a -> 'a option

Options can be used to model partial functions. If a function of type $\alpha\rightarrow\beta$ does not have useful $\beta$ values for all $\alpha$ inputs, then this distinction can be marked by making the range of the function $\beta\,$option, and mapping the undefined $\alpha$ values to NONE.

An inductive type, options have a recursion theorem supporting the definition of primitive recursive functions over option values.

option_Axiom
  ⊢ ∀e f. ∃fn. fn NONE = e ∧ ∀x. fn (SOME x) = f x

The option theory also defines a case constant that allows one to inspect option values in a “pattern-matching” style.

   case e of
     NONE => u
   | SOME x => f x

The constant underlying this syntactic sugar is option_CASE with definition

option_case_def
  ⊢ (∀v f. option_CASE NONE v f = v) ∧
     ∀x v f. option_CASE (SOME x) v f = f x

Another useful function maps a function over an option:

OPTION_MAP_DEF
  ⊢ (∀f x. OPTION_MAP f (SOME x) = SOME (f x)) ∧
     ∀f. OPTION_MAP f NONE = NONE

Finally, the THE function takes a SOME value to that constructor's argument, and is unspecified on NONE:

THE_DEF
  ⊢ ∀x. THE (SOME x) = x

Numbers

The natural numbers, integers, and real numbers are provided in a series of theories. Also available are theories of extended real numbers, $n$-bit words (numbers modulo $2^n$), floating point and fixed point numbers.

Natural numbers

The natural numbers are developed in a series of theories: num, prim_rec, arithmetic, and numeral. In num, the type of numbers is defined from the Axiom of Infinity, and Peano's axioms are derived. In prim_rec the Primitive Recursion theorem is proved. Based on that, a large theory treating the standard arithmetic operations is developed in arithmetic. Lastly, a theory of numerals is developed in numeral.

The theory num

The theory num defines the type num of natural numbers to be isomorphic to a countable subset of the primitive type ind. In this theory, the constants 0 and SUC (the successor function) are defined and Peano's axioms pre-proved in the form:

NOT_SUC
  ⊢ ∀n. SUC n ≠ 0
INV_SUC
  ⊢ ∀m n. SUC m = SUC n ⇒ m = n
INDUCTION
  ⊢ ∀P. P 0 ∧ (∀n. P n ⇒ P (SUC n)) ⇒ ∀n. P n

In higher order logic, Peano's axioms are sufficient for developing number theory because addition and multiplication can be defined. In first order logic these must be taken as primitive. Note also that INDUCTION could not be stated as a single axiom in first order logic because predicates (e.g., P) cannot be quantified.

The theory prim_rec

In classical logic, unlike domain theory logics such as PP$\lambda$, arbitrary recursive definitions are not allowed. For example, there is no function $f$ (of type num->num) such that

   !x. f x = (f x) + 1

Certain restricted forms of recursive definition do, however, uniquely define functions. An important example are the primitive recursive functions.6 For any $x$ and $f$ the primitive recursion theorem tells us that there is a unique function fn such that:

   (fn 0 = x) /\ (!n. fn(SUC n) = f (fn n) n)

The primitive recursion theorem, named num_Axiom in HOL, follows from Peano's axioms.

num_Axiom
  ⊢ ∀e f. ∃fn. fn 0 = e ∧ ∀n. fn (SUC n) = f n (fn n)

The theorem states the validity of primitive recursive definitions on the natural numbers: for any x and f there exists a corresponding total function fn which satisfies the primitive recursive definition whose form is determined by x and f.

The less-than relation. The less-than relation '<' is most naturally defined by primitive recursion. However, in our development it is needed for the proof of the primitive recursion theorem, so it must be defined before definition by primitive recursion is available. The theory prim_rec therefore contains the following non-recursive definition of <:

LESS_DEF
  ⊢ ∀m n. m < n ⇔ ∃P. (∀n. P (SUC n) ⇒ P n) ∧ P m ∧ ¬P n

This definition says that m < n if there exists a set (with characteristic function P) that is downward closed7 and contains m but not n.

Mechanizing primitive recursive definitions

The primitive recursion theorem can be used to justify any definition of a function on the natural numbers by primitive recursion. For example, a primitive recursive definition in higher order logic of the form

$$ \begin{aligned} \mathtt{fun}\,0\,x_1\,\ldots\,x_i &= f_1[x_1,\ldots,x_i]\\ \mathtt{fun}\,(\mathtt{SUC}\,n)\,x_1\,\ldots\,x_i &= f_2[\mathtt{fun}\,n\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i] \end{aligned} $$

where all the free variables in the terms $t_1,\ldots,t_i$ are contained in $\{n,x_1,\ldots,x_i\}$, is logically equivalent to:

$$ \begin{aligned} \mathtt{fun}\,0 &= \lambda x_1\,\ldots\,x_i.\,f_1[x_1,\ldots,x_i]\\ \mathtt{fun}\,(\mathtt{SUC}\,n) &= \lambda x_1\,\ldots\,x_i.\,f_2[\mathtt{fun}\,n\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i]\\ &= (\lambda f\,n\,x_1\,\ldots\,x_i.\,f_2[f\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i])\,(\mathtt{fun}\,n)\,n \end{aligned} $$

The existence of a recursive function fun which satisfies these two equations follows directly from the primitive recursion theorem num_Axiom shown above. Specializing the quantified variables x and f in a suitably type-instantiated version of num_Axiom so that

$$ \begin{aligned} x &= \lambda x_1\,\ldots\,x_i.\,f_1[x_1,\ldots,x_i]\quad\text{and}\\ f &= \lambda f\,n\,x_1\,\ldots\,x_i.\,f_2[f\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i] \end{aligned} $$

yields the existence theorem shown below:

$$ \begin{aligned} \vdash\,\exists\mathtt{fn}.\, &\mathtt{fn}\,0 = \lambda x_1\,\ldots\,x_i.\,f_1[x_1,\ldots,x_i]\;\wedge\\ &\mathtt{fn}\,(\mathtt{SUC}\,n) = (\lambda f\,n\,x_1\,\ldots\,x_i.\,f_2[f\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i])\,(\mathtt{fn}\,n)\,n \end{aligned} $$

This theorem allows a constant fun to be introduced (via the definitional mechanism of constant specifications—see Section 1.6.3.2) to denote the recursive function that satisfies the two equations in the body of the theorem. Introducing a constant fun to name the function asserted to exist by the theorem shown above, and simplifying using $\beta$-reduction, yields the following theorem:

$$ \begin{aligned} \vdash\, &\mathtt{fun}\,0 = \lambda x_1\,\ldots\,x_i.\,f_1[x_1,\ldots,x_i]\;\wedge\\ &\mathtt{fun}\,(\mathtt{SUC}\,n) = \lambda x_1\,\ldots\,x_i.\,f_2[\mathtt{fun}\,n\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i] \end{aligned} $$

It follows immediately from this theorem that the constant fun satisfies the primitive recursive defining equations given by the theorem shown below:

$$ \begin{aligned} \vdash\, &\mathtt{fun}\,0\,x_1\,\ldots\,x_i = f_1[x_1,\ldots,x_i]\\ &\mathtt{fun}\,(\mathtt{SUC}\,n)\,x_1\,\ldots\,x_i = f_2[\mathtt{fun}\,n\,t_1\,\ldots\,t_i,\,n,\,x_1,\ldots,x_i] \end{aligned} $$

To automate the use of the primitive recursion theorem in deriving recursive definitions of this kind, the HOL system provides a function which automatically proves the existence of primitive recursive functions and then makes a constant specification to introduce the constant that denotes such a function:

   new_recursive_definition :
      {def : term, name : string, rec_axiom : thm} -> thm

In fact, new_recursive_definition handles primitive recursive definitions over a range of types, not just the natural numbers. For details, see the REFERENCE documentation.

More conveniently still, the Define function (see Section 8.3.1) supports primitive recursion, along with other styles of recursion, and does not require the user to quote the primitive recursion axiom. It may, however, require termination proofs to be performed; fortunately, these need not be done for primitive recursions.

Dependent choice and wellfoundedness

The primitive recursion theorem is useful beyond its main purpose of justifying recursive definitions. For example, the theory prim_rec proves the Axiom of Dependent Choice (DC).

DC
  ⊢ ∀P R a.
       P a ∧ (∀x. P x ⇒ ∃y. P y ∧ R x y) ⇒
       ∃f. f 0 = a ∧ ∀n. P (f n) ∧ R (f n) (f (SUC n))

The proof uses SELECT_AX. The theorem DC is useful when one wishes to build a function having a certain property from a relation. For example, one way to define the wellfoundedness of a relation $R$ is to say that it has no infinite decreasing $R$ chains.

wellfounded_def
  ⊢ ∀R. Wellfounded R ⇔ ¬∃f. ∀n. R (f (SUC n)) (f n)
WF_IFF_WELLFOUNDED
  ⊢ ∀R. WF R ⇔ Wellfounded R

By use of DC, this statement can be proved to be equal to the notion of wellfoundedness WF (namely, that every set has an $R$-minimal element) defined in the theory relation.

Theorems asserting the wellfoundedness of the predecessor relation and the less-than relation, as well as the wellfoundedness of measure functions are also proved in prim_rec.

WF_PRED
  ⊢ WF (λx y. y = SUC x)
WF_LESS
  ⊢ WF $<

measure_def
  ⊢ measure = inv_image $<
measure_thm
  ⊢ ∀f x y. measure f x y ⇔ f x < f y
WF_measure
  ⊢ ∀m. WF (measure m)

Arithmetic

The HOL theory arithmetic contains primitive recursive definitions of the following standard arithmetic operators.

ADD
  ⊢ (∀n. 0 + n = n) ∧ ∀m n. SUC m + n = SUC (m + n)

SUB
  ⊢ (∀m. 0 − m = 0) ∧
     ∀m n. SUC m − n = if m < n then 0 else SUC (m − n)

MULT
  ⊢ (∀n. 0 * n = 0) ∧ ∀m n. SUC m * n = m * n + n

EXP
  ⊢ (∀m. m ** 0 = 1) ∧ ∀m n. m ** SUC n = m * m ** n

Note that EXP is an infix. The infix notation ** may be used in place of EXP. Thus (x EXP y) means $x^y$, and so does (x ** y). In addition, the parser special-cases superscript 2 and 3 notations, so that `x²` is actually the same term as x EXP 2, and `x³` is the same term as x EXP 3.

Comparison operators. A full set of comparison operators is defined in terms of <.

GREATER_DEF
  ⊢ ∀m n. m > n ⇔ n < m
LESS_OR_EQ
  ⊢ ∀m n. m ≤ n ⇔ m < n ∨ m = n
GREATER_OR_EQ
  ⊢ ∀m n. m ≥ n ⇔ m > n ∨ m = n

Note that in all of HOL's standard numeric theories, it is usual practice to avoid uses of the “greater-than” constants and to express everything with either $<$ or $\le$.

Division and modulus. A constant specification is used to introduce division (DIV, infix) and modulus (MOD, infix) operators, together with their characterizing property.

DIVISION
  ⊢ ∀n. 0 < n ⇒ ∀k. k = k DIV n * n + k MOD n ∧ k MOD n < n

Even and odd. The properties of a number being even or odd are defined recursively.

EVEN
  ⊢ (EVEN 0 ⇔ T) ∧ ∀n. EVEN (SUC n) ⇔ ¬EVEN n

ODD
  ⊢ (ODD 0 ⇔ F) ∧ ∀n. ODD (SUC n) ⇔ ¬ODD n

Maximum and minimum. The minimum and maximum of two numbers are defined in the usual way.

MAX_DEF
  ⊢ ∀m n. MAX m n = if m < n then n else m
MIN_DEF
  ⊢ ∀m n. MIN m n = if m < n then m else n

Factorial. The factorial of a number is a primitive recursive definition.

FACT
  ⊢ FACT 0 = 1 ∧ ∀n. FACT (SUC n) = SUC n * FACT n

Function iteration. The iterated application $f^n(x)$ of a function $f:\alpha\to\alpha$ is defined by primitive recursion. The definition (FUNPOW) is tail-recursive, which can be awkward to reason about. An alternative characterization (FUNPOW_SUC) may be easier to apply when doing proofs.

FUNPOW
  ⊢ (∀f x. FUNPOW f 0 x = x) ∧
     ∀f n x. FUNPOW f (SUC n) x = FUNPOW f n (f x)
FUNPOW_SUC
  ⊢ ∀f n x. FUNPOW f (SUC n) x = f (FUNPOW f n x)

On this basis, an ad hoc but useful collection of over two hundred and fifty elementary theorems of arithmetic are proved when HOL is built and stored in the theory arithmetic. For a complete list of the available theorems, see the REFERENCE manual. See also Section 5.6 for discussion of the LEAST operator, which returns the least number satisfying a predicate.

Grammar information

The following table gives the parsing status of the arithmetic constants.

OperatorStrengthAssociativity
>=450non
<=450non
>450non
<450non
+500left
-500left
*600left
DIV600left
MOD650left
EXP700right

Numerals

The type num is usually thought of as being supplied with an infinite collection of numerals: 1, 2, 3, etc. However, the HOL logic has no way to define such infinite families of constants; instead, all numerals other than $0$ are actually built up from the constants introduced by the following definitions:

NUMERAL_DEF
  ⊢ ∀x. NUMERAL x = x

BIT1
  ⊢ ∀n. BIT1 n = n + (n + SUC 0)
BIT2
  ⊢ ∀n. BIT2 n = n + (n + SUC (SUC 0))

ALT_ZERO
  ⊢ ZERO = 0

For example, the numeral $5$ is represented by the term

$$ \mathtt{NUMERAL}(\mathtt{BIT1}(\mathtt{BIT2}\;\mathtt{ZERO})) $$

and the HOL parser and pretty-printer make such terms appear as numerals. This binary representation for numerals allows for asymptotically efficient calculation. Theorems supporting arithmetic calculations on numerals can be found in the numeral theory; these are mechanized by the reduce library. Thus, arithmetic calculations are performed by deductive steps in HOL. For example the following calculation of $2^{(1023+14)/9}$ takes approximately 4,200 primitive inference steps and returns quickly:

> Count.apply reduceLib.REDUCE_CONV ``2 EXP ((1023 + 14) DIV 9)``;
runtime: 0.00063s,    gctime: 0.00000s,     systime: 0.00001s.
Axioms: 0, Defs: 0, Disk: 0, Orcl: 0, Prims: 4202; Total: 4202
val it =
   ⊢ 2 ** ((1023 + 14) DIV 9) =
     41538374868278621028243970633760768: thm

Construction of numerals. Numerals may of course be built using mk_comb, and taken apart with dest_comb; however, a more convenient interface to this functionality is provided by the functions mk_numeral, dest_numeral, and is_numeral (found in the structure numSyntax). These entry-points make use of an ML structure Arbnum which implements arbitrary precision numbers num. The following session shows how HOL numerals are constructed from elements of type num and how numerals are destructed. The structure Arbnum provides a full collection of arithmetic operations, using the usual names for the operations, e.g., +, *, -, etc.

> numSyntax.mk_numeral
    (Arbnum.fromString "3432432423423423234");
val it = “3432432423423423234”: term

> numSyntax.dest_numeral it;
val it = 3432432423423423234: num

> Arbnum.+(it,it);
val it = 6864864846846846468: num

Numerals and the parser. Simple digit sequences are parsed as decimal numbers, but the parser also supports the input of numbers in binary, octal and hexadecimal notation. Numbers may be written in binary and hexadecimal form by prefixing them with the strings 0b and 0x respectively. The ‘digits’ AF in hexadecimal numbers may be written in upper or lower case. Binary numbers have their most significant digits left-most. In the interests of backwards compatibility, octal numbers are not enabled by default, but if the reference base_tokens.allow_octal_input is set to true, then octal numbers are those that appear with leading zeroes.

Finally, all numbers may be padded with underscore characters (_). These can be used to groups digits for added legibility and have no semantic effect.

Thus

> ``0xAA``;
val it = “170”: term

> ``0b1010_1011``;
val it = “171”: term

> base_tokens.allow_octal_input := true;
val it = (): unit

> ``067``;
val it = “55”: term

Numerals and Peano numbers. Numerals are related to numbers built from 0 and SUC via the derived inference rule num_CONV, found in the numLib library.

   num_CONV : term -> thm

num_CONV can be used to generate the 'SUC' equation for any non-zero numeral. For example:

> open numLib;   ... output elided ...
> num_CONV ``2``;
val it = ⊢ 2 = SUC 1: thm

> num_CONV ``3141592653``;
val it = ⊢ 3141592653 = SUC 3141592652: thm

The num_CONV function works purely by inference.

Overloading of arithmetic operators

When other numeric theories are loaded (such as those for the reals or integers), numerals are overloaded so that the numeral 1 can actually stand for a natural number, an integer or a real value. The parser has a pass of overloading resolution in which it attempts to determine the actual type to give to a numeral. For example, in the following session, the theory of integers is loaded, whereupon the numeral 2 is taken to be an integer.

> load "integerTheory";
val it = (): unit

> ``2``;
<<HOL message: more than one resolution of overloading was possible>>
val it = “2”: term

> type_of it;
val it = “:int”: hol_type

In order to precisely specify the desired type, the user can use single character suffixes ('n' for the natural numbers, and 'i' for the integers):

> type_of ``2n``;
val it = “:num”: hol_type

> type_of ``42i``;
val it = “:int”: hol_type

A numeric literal for a HOL type other than num, such as 42i, is represented by the application of an injection function of type num -> ty to a numeral. The injection function is different for each type ty. See Section 5.3.4 for further discussion.

The functions mk_numeral, dest_numeral, and is_numeral only work for numerals, and not for numeric literals with character suffixes other than n. For information on how to install new character suffixes, consult the add_numeral_form entry in the REFERENCE manual.

Integers

There is an extensive theory of integers in HOL. The type of integers is constructed as a quotient on pairs of natural numbers. A standard collection of operators are defined. These are overloaded with similar operations on the natural numbers, and on the real numbers. The constants defined in the integer theory include those found in the following table.

ConstantOverloaded symbolStrengthAssociativity
int_ge>=450non
int_le<=450non
int_gt>450non
int_lt<450non
int_add+500left
int_sub-500left
int_mul*600left
/600left
%650left
int_exp**700right
int_of_num&900prefix
int_neg~900prefix

The overloaded symbol & : num -> int denotes the injection function from natural numbers to integers. The following session illustrates how overloading and integers literals are treated.

> “1i = &(1n + 0n)”;
val it = “1 = &(1 + 0)”: term

> show_numeral_types := true;
val it = (): unit

> “&1 = &(1n + 0n)”;
<<HOL message: more than one resolution of overloading was possible>>
val it = “1i = &(1n + 0n)”: term
> show_numeral_types := false;   ... output elided ...

In addition, there is an absolute value function ABS:

integerTheory.INT_ABS
  ⊢ ∀n. ABS n = if n < 0 then -n else n

with the obvious definition.

This then characterises the Num function which maps from integers back into natural numbers (of type :int -> num therefore):

integerTheory.Num_EQ_ABS
  ⊢ ∀i. &Num i = ABS i

Rational numbers

The type of rationals is constructed as a quotient on ordered pairs of integers (the numerator and the denominator of a fraction) whose second component must not be zero. To make things easier in the HOL theory, the sign of a rational number is always moved to the numerator. So, the denominator is always positive.

A standard collection of operators, which are overloaded with similar operations on the integers, are defined. These include those found in the following table. Injection from natural numbers is supported by the overloaded symbol & : num -> rat and the suffix q.

ConstantOverloaded symbolStrengthAssociativity
rat_geq>=450non
rat_leq<=450non
rat_gre>450non
rat_les<450non
rat_add+500left
rat_sub-500left
rat_minv
rat_mul*600left
rat_div/600left
rat_ainv~900prefix
rat_of_num&900prefix

The theorems in the theory of rational numbers include field properties, arithmetic rules, manipulation of (in)equations and their reduction to (in)equations between integers, properties of less-than relations and the density of rational numbers. For details, consult the REFERENCE manual and the source files.

Real numbers

There is an extensive collection of theories that make up the development of real numbers and analysis in HOL, due to John Harrison (Harrison 1998). We will only give a sketchy overview of the development; the interested reader should consult the REFERENCE manual and Harrison's thesis.

The axioms for the real numbers are derived from the ‘half reals’ which are constructed from the ‘half rationals’. This part of the development is recorded in hratTheory and hrealTheory, but is not used once the reals have been constructed. The real axioms are derived in the theory realaxTheory. A standard collection of operators on the reals, and theorems about them, is found in realaxTheory and realTheory. The operators and their parse status are listed in the following table.

ConstantOverloaded symbolStrengthAssociativity
real_ge>=450non
real_lte<=450non
real_gt>450non
real_lt<450non
real_add+500left
real_sub-500left
real_mul*600left
real_div/600left
pow700right
rpow700right
real_of_num&900prefix
real_neg~900prefix

On the basis of realTheory, the following sequence of theories is constructed:

real_sigma
Summation of real numbers (the $\Sigma$ operator, etc.)
topology
General topology.
metric
Metric spaces, including metric on the real line.
nets
Moore-Smith convergence nets, and special cases like sequences.
real_topology
Topology of one-dimensional Euclidean space (Section 6.2).
seq
Sequences and series of real numbers.
derivative
The new univariate differential calculus (Section 6.3).
lim
Limits, continuity and the old differentiation.
powser
Power series.
transc
Transcendental functions, e.g., exp, sin, cos, ln, root, sqrt, pi, tan, asn, acs, atn.
integration
The new univariate integral calculus (Section 6.3).
integral
The old univariate integral calculus.

HOL also includes a basic theory of the complex numbers (complexTheory), where the type complex is a type abbreviation for a pair of real numbers. The $\sqrt{-1}$ value is the HOL constant i. Numerals are supported (with the suffix c available to force numerals to be parsed as complex numbers). The standard arithmetic operations are defined, with the appropriate theorems proved about them.

Extended real numbers

The HOL provides an extensive theory of extended real numbers (extreal), originally developed by T. Mhamdi, O. Hasan, and S. Tahar (Mhamdi, Hasan, and Tahar 2011). With extended reals, the limit of a monotonic sequence is always defined, infinite when the sequence is divergent, but still defined and properties can be proven on it.

It is often helpful to use the values $+\infty$ and $-\infty$ in calculations. To do this properly, we have to consider the extended real line $\overline{\mathbb{R}} := [-\infty, +\infty]$. If we agree that $-\infty < x$ and $y < +\infty$ for all $x, y \in \mathbb{R}$, then $\overline{\mathbb{R}}$ inherits the ordering from $\mathbb{R}$ as well as the usual rules of addition, subtraction, multiplication and division of elements from $\mathbb{R}$. The latter needs to be augmented as shown in Table 5.3.7 (Schilling 2017, 61):

Table: $+$, $-$, $\cdot$ and $/$ in $\overline{\mathbb{R}}$, where $x, y \in \mathbb{R}$ and $a, b \in (0, \infty)$.

Addition.

$+$0$y$$+\infty$$-\infty$
00$y$$+\infty$$-\infty$
$x$$x$$x+y$$+\infty$$-\infty$
$+\infty$$+\infty$$+\infty$$+\infty$$\nexists$
$-\infty$$-\infty$$-\infty$$\nexists$$-\infty$

Subtraction.

$-$0$y$$+\infty$$-\infty$
00$-y$$-\infty$$+\infty$
$x$$x$$x-y$$-\infty$$+\infty$
$+\infty$$+\infty$$+\infty$$\nexists$$+\infty$
$-\infty$$-\infty$$-\infty$$-\infty$$\nexists$

Multiplication.

$\cdot$0$\pm b$$+\infty$$-\infty$
00000
$\pm a$0$a\cdot b$$\pm\infty$$\mp\infty$
$+\infty$0$\pm\infty$$+\infty$$-\infty$
$-\infty$0$\mp\infty$$-\infty$$+\infty$

Division.

$/$0$\pm b$$+\infty$$-\infty$
0$\nexists$000
$\pm a$$\nexists$$a/b$00
$+\infty$$\nexists$$\pm\infty$$\nexists$$\nexists$
$-\infty$$\nexists$$\mp\infty$$\nexists$$\nexists$

In HOL, the type of extended real numbers (extreals hereafter) is constructed by an algebraic datatype extreal (see Section 7.2):

   Datatype `extreal = NegInf | PosInf | Normal real`

Thus Normal r denotes the extreal corresponding to the real number r, while PosInf and NegInf denote $+\infty$ and $-\infty$, respectively. In order to precisely specify extreals corresponding to natural numbers, the user can use the single character suffix 'x':

> type_of ``0x``;
val it = “:extreal”: hol_type

The function real can be used to convert extreals back to the corresponding reals ($+\infty$ and $-\infty$ are mapped to 0x).

A standard collection of arithmetic operators8 and elementary functions on the extreals are defined and overloaded on the corresponding operators of real numbers, shown in Table 5.3.7.

Table: Arithmetic operators and transcendental functions for extreals.

ConstantOverloaded symbolStrengthAssociativity
extreal_le<=450non
extreal_lt<450non
extreal_add+500left
extreal_sub-500left
extreal_mul*600left
extreal_div/600left
extreal_of_num&900prefix
extreal_ainv~ and -900prefix
extreal_invinvprefix
extreal_absabsprefix
extreal_powpow700right
extreal_powrpowr700right
extreal_expexpprefix
extreal_sqrtsqrtprefix
extreal_logrlogrprefix
extreal_lglgprefix
extreal_lnlnprefix

The addition of extreals is not associative and commutative in general, because PosInf + NegInf and NegInf + PosInf are not defined (see Table 5.3.7). To swap the elements of additions, the user must avoid mixing of PosInf and NegInf in the involved elements (e.g., by letting one of them be normal):

add_comm
  ⊢ ∀x y. x ≠ −∞ ∧ y ≠ −∞ ∨ x ≠ +∞ ∧ y ≠ +∞ ⇒ x + y = y + x
add_comm_normal
  ⊢ ∀x y. Normal x + y = y + Normal x
add_assoc
  ⊢ ∀x y z.
       x ≠ −∞ ∧ y ≠ −∞ ∧ z ≠ −∞ ∨ x ≠ +∞ ∧ y ≠ +∞ ∧ z ≠ +∞ ⇒
       x + (y + z) = x + y + z

On the other hand, the set of extreals is a totally ordered set such that for all $a \in \overline{\mathbb{R}}$, $-\infty \leq a \leq +\infty$. With this order, $\overline{\mathbb{R}}$ is a complete lattice where every subset has a supremum (extreal_sup or sup) and an infimum (extreal_inf or inf). In particular, for empty sets (of extreals) we have:

sup_empty
  ⊢ sup ∅ = −∞
inf_empty
  ⊢ inf ∅ = +∞

Finite and infinite sum of extreals. The sum of extreals over a finite set (EXTREAL_SUM_IMAGE, overloaded on SIGMA), $\sum_{i\in s} f(i)$, is defined by pred_set.ITSET (see Section 5.5.1):

EXTREAL_SUM_IMAGE_DEF
  ⊢ ∀f s. ∑ f s = ITSET (λe acc. f e + acc) s 0

To actually work with EXTREAL_SUM_IMAGE, beside that $s$ must be a finite set, there must be no mixing of PosInf and NegInf in the values of f, i.e., either all $f(i)$ are not $+\infty$ or they are not $-\infty$ ($i \in s$). The following theorem fully captures the properties of EXTREAL_SUM_IMAGE:

EXTREAL_SUM_IMAGE_THM
  ⊢ ∀f. ∑ f ∅ = 0 ∧ (∀e. ∑ f {e} = f e) ∧
         ∀e s.
           FINITE s ∧
           ((∀x. x ∈ e INSERT s ⇒ f x ≠ +∞) ∨
            ∀x. x ∈ e INSERT s ⇒ f x ≠ −∞) ⇒
           ∑ f (e INSERT s) = f e + ∑ f (s DELETE e)

The (countably) infinite sum of extreals (ext_suminf, overloaded on suminf), $\sum_{i\in\mathbb{N}} f(i)$, is only defined on non-negative function $f$ as the supremum of the $n$th partial sum:

ext_suminf_def
  ⊢ ∀f. (∀n. 0 ≤ f n) ⇒
         suminf f = sup (IMAGE (λn. ∑ f (count n)) 𝕌(:num))

Thus mathematically ext_suminf represents positive series, which always has a unique nonnegative value: PosInf if the positive series is divergent, other normal extreals (i.e., < PosInf) if the positive series is convergent (on that normal extreal). A fundamental result for positive series says that it converges if and only if its $n$th partial sums are bounded:

pos_summable
  ⊢ ∀f. (∀n. 0 ≤ f n) ∧ (∃r. ∀n. ∑ f (count n) ≤ Normal r) ⇒
         suminf f < +∞

Finally, 2-dimensional (positive) infinite sums $\sum_{i,j\in\mathbb{N}} f(i,j)$ can be reduced to iterated sums $\sum_{i\in\mathbb{N}} \sum_{j\in\mathbb{N}} f(i,j)$ given an arbitrary bijection between $\mathbb{N}$ and $\mathbb{N}\times\mathbb{N}$:

ext_suminf_2d_full
  ⊢ ∀f g h.
       (∀m n. 0 ≤ f m n) ∧ (∀n. suminf (f n) = g n) ∧
       BIJ h 𝕌(:num) (𝕌(:num) × 𝕌(:num)) ⇒
       suminf (UNCURRY f ∘ h) = suminf g

Upper and lower limits of extreal sequences. For a sequence of extreal numbers, the limes inferior or lower limit is defined as (see, e.g., Appendix A of (Schilling 2017) for more details.)

ext_liminf_def
  ⊢ ∀a. liminf a = sup (IMAGE (λm. inf {a n | m ≤ n}) 𝕌(:num))

and the limes superior or upper limit is defined as

ext_limsup_def
  ⊢ ∀a. limsup a = inf (IMAGE (λm. sup {a n | m ≤ n}) 𝕌(:num))

Some basic properties of ext_limsup and ext_liminf are provided in extreal theory:

ext_liminf_alt_limsup
  ⊢ ∀a. liminf a = -limsup (numeric_negate ∘ a)
ext_liminf_pos
  ⊢ ∀a. (∀n. 0 ≤ a n) ⇒ 0 ≤ liminf a
ext_liminf_le_limsup
  ⊢ ∀a. liminf a ≤ limsup a
ext_limsup_alt_liminf
  ⊢ ∀a. limsup a = -liminf (numeric_negate ∘ a)
ext_limsup_pos
  ⊢ ∀a. (∀n. 0 ≤ a n) ⇒ 0 ≤ limsup a

The most important property, however, that the normal limit of a sequence of extreal numbers (when forcely converted to real numbers) coincides with its upper and lower limits (in this case they are also the same), together with useful lemmas, are provided in martingale theory:

ext_limsup_thm
  ⊢ ∀a l.
       (∀n. a n ≠ +∞ ∧ a n ≠ −∞) ⇒
       ((real ∘ a ⟶ l) sequentially ⇔
        limsup a = Normal l ∧ liminf a = Normal l)

ext_limsup_le_subseq
  ⊢ ∀a f l.
       (∀n. a n ≠ +∞ ∧ a n ≠ −∞) ∧ (∀m n. m < n ⇒ f m < f n) ∧
       (real ∘ a ∘ f ⟶ l) sequentially ⇒
       Normal l ≤ limsup a
ext_liminf_le_subseq
  ⊢ ∀a f l.
       (∀n. a n ≠ +∞ ∧ a n ≠ −∞) ∧ (∀m n. m < n ⇒ f m < f n) ∧
       (real ∘ a ∘ f ⟶ l) sequentially ⇒
       liminf a ≤ Normal l
ext_limsup_imp_subseq
  ⊢ ∀a. (∀n. a n ≠ +∞ ∧ a n ≠ −∞) ∧ limsup a ≠ +∞ ∧
         limsup a ≠ −∞ ⇒
         ∃f. (∀m n. m < n ⇒ f m < f n) ∧
             (real ∘ a ∘ f ⟶ real (limsup a)) sequentially
ext_liminf_imp_subseq
  ⊢ ∀a. (∀n. a n ≠ +∞ ∧ a n ≠ −∞) ∧ liminf a ≠ +∞ ∧
         liminf a ≠ −∞ ⇒
         ∃f. (∀m n. m < n ⇒ f m < f n) ∧
             (real ∘ a ∘ f ⟶ real (liminf a)) sequentially

Bit vectors

HOL provides a theory of bit vectors, or $n$-bit words. For example, in computer architectures one finds: bytes/octets ($n=8$), half-words ($n=16$), words ($n=32$) and long-words ($n=64$). In the theory words, bit vectors are represented as finite Cartesian products: an $n$-bit word is given type $\worda$ where the size of the type $\alpha$ determines the word length $n$. This approach comes from an idea of John Harrison, which was presented at TPHOLs 2005.9

Finite Cartesian products

The HOL theory fcp introduces an infix type operator **, which is used to represent finite Cartesian products.10 The type 'a ** 'b, or equivalently $\fcp{\mathit{a}}{\mathit{b}}$, is conceptually equivalent to:

$$ \underbrace{\mathit{a}\;\#\;\mathit{a}\;\#\;\cdots\;\#\;\mathit{a}}_{\mathtt{dimindex('b)}} $$

where dimindex('b) is the cardinality of univ(:'b) when 'b is finite and is one when it is infinite. Thus, $\fcp{\mathit{a}}{\mathit{num}}$ is similar to 'a, and $\fcp{\mathit{a}}{\mathit{bool}}$ is similar to 'a # 'a. Numeral type names are supported, so one can freely work with indexing sets of any size, e.g., the type 32 has thirty-two elements and $\fcp{\mathit{bool}}{32}$ represents 32-bit words.

The components of a finite Cartesian product are accessed with an indexing function

   fcp_index : 'a ** 'b -> num -> 'a

which is typically written with an infixed apostrophe: x ' i denotes the value of vector x at position i. Typically, indices are constrained to be less than the size of 'b.11

The following theorem shows that two Cartesian products x and y are equal if, and only if, all of their components x ' i and y ' i are equal:

CART_EQ
  ⊢ ∀x y. x = y ⇔ ∀i. i < dimindex (:β) ⇒ x ' i = y ' i

In order to construct Cartesian products, the theory fcp introduces a binder FCP, which is characterised by the following theorems:

FCP_BETA
  ⊢ ∀i. i < dimindex (:β) ⇒ $FCP g ' i = g i
FCP_ETA
  ⊢ ∀g. (FCP i. g ' i) = g

The theorem FCP_BETA shows that the components of $FCP g are determined by the function g:num -> 'a. The theorem FCP_ETA shows that a binding can be eliminated when all of the components are identical to that of x. These two theorems, together with CART_EQ, can be found in the simpset fragment fcpLib.FCP_ss.

Finite Cartesian products provide a good means to model $n$-bit words. That is to say, the type $\fcp{\mathit{bool}}{\mathit{a}}$ can represent a binary word whose length $n$ corresponds with the size of the type 'a. The binder FCP provides a flexible means for defining words — one can supply a function f:num -> bool that gives the word's bit values, each of which can be accessed using the indexing map fcp_index.

Bit theory

The theory bit defines some bit operations over the natural numbers, e.g., BITS, SLICE, BIT, BITWISE and BIT_MODIFY. In this context, natural numbers are treated as binary words of unbounded length. The operations in bit are primarily defined using DIV, MOD and EXP. For example, from the definition of BIT, the following theorem holds:

BIT_DEF
  ⊢ ∀b n. BIT b n ⇔ n DIV 2 ** b MOD 2 = 1

Here BIT b n states that the $b$-th bit (counted from the least significant bit, starting by 0) of $n$ is 1. (In other words, BIT b n maps the $b$-th bit of $n$ from 0 to false and 1 to true.)

On the other hand, SBIT can be used to construct a number by summing up values corresponding to each bits. SBIT b n represents the single $n$-th bit value indicated by the Boolean value $b$, to be accumulated for constructing the destination number:

SBIT_def
  ⊢ ∀b n. SBIT b n = if b then 2 ** n else 0

This theory is used in the development of the word theory and it also provides a mechanism for the efficient evaluation of some word operations via the theory numeral_bit.

Words theory

The theory words introduces a selection of polymorphic constants and operations, which can be type instantiated to any word size. For example, word addition has type:

$$ +:\worda \to \worda \to \worda $$

If 'a is instantiated to 32 then this operation corresponds with 32-bit addition. All theorems about word operations apply for any word length.12

Some basic operations. The function w2n: $\worda \to \mathit{num}$ gives the natural number value of a word. If $x \in \mathit{bool}^{\{0,1,\ldots,n-1\}}$ is a finite Cartesian product representing an $n$-bit word then its natural number value is:

$$ \mathrm{w2n}(x) = \sum_{i=0}^{n-1}\textbf{if } x_i\textbf{ then } 2^i\textbf{ else } 0\,. $$

The length of a word (the number $n$) is given by the function word_len: $\worda \to \mathit{num}$. The function n2w: $\mathit{num} \to \worda$ maps from a number to a word, and the function w2n: $\worda \to \mathit{num}$ maps from a word to a number. They are defined in HOL by:

n2w_def
  ⊢ ∀n. n2w n = FCP i. BIT i n
w2n_def
  ⊢ ∀w. w2n w = SUM (dimindex (:α)) (λi. SBIT (w ' i) i)

The suffix w is used to denote word literals, e.g., 255w is the same as n2w 255.

The function w2w: $\worda \to \wordb$ provides word-to-word conversion (casting):

w2w_def
  ⊢ ∀w. w2w w = n2w (w2n w)

If $\beta$ is smaller than $\alpha$ then the higher bits of w will be lost (it performs bit extraction), otherwise the longer word will have the same value as the original (in effect providing zero padding). However, if one were treating w as a two's complement number then the word needs to be sign extended, i.e.,

$$ \begin{aligned} \text{($-$ve)}\quad &1 b_{n-2}\cdots b_0 \;\mapsto\; 1\cdots 11 b_{n-2}\cdots b_0\\ \text{($+$ve)}\quad &0 b_{n-2}\cdots b_0 \;\mapsto\; 0\cdots 00 b_{n-2}\cdots b_0 \end{aligned} $$

The function sw2sw: $\worda \to \wordb$ provides this sign extending version of w2w.

A collection of operations are provided for mapping to and from strings and number (digit) lists, e.g.,

|- word_to_dec_string 876w = "876"

and

|- word_to_hex_list 876w = [12; 6; 3]

These function are specialised versions of w2s and w2l respectively.

Concatenation. The operation word_concat: $\worda \to \wordb \to \wordc$ concatenates words. Note that the return type is not constrained. This means that two sixteen bit words can be concatenated to give a word of any length — which may be smaller or larger than the expected value of 32. The related function word_join does return a word of the expected length, i.e., of type $\fcp{\mathit{bool}}{\alpha+\beta}$; however, the concatenation operation is more useful because we often want $\fcp{\mathit{bool}}{32}$ and not the logically distinct $\fcp{\mathit{bool}}{16+16}$.

Signed and unsigned words. Words can be viewed as being either signed (using the two's complement representation) or as being unsigned. However, this is not made explicit within the theory13 and all of the arithmetic operations are defined using the natural numbers, i.e., via w2n and n2w. In particular, addition and multiplication work naturally (have the same definition) under the two's complement representation. This is not the case however with word-to-word conversion, orderings, division and right shifting, where signed and unsigned variants are needed. When operating over the natural numbers, some of the two's complement versions have slightly unnatural looking presentations. For example, with the signed (two's complement) version of “less than” we have 255w < (0w:word8) because the word 255w is actually taken to be representing the integer $-1$, whereas the unsigned version is more natural: 0w <+ (255w:word8).

Bit field operations. The standard Boolean bit field operations are provided, i.e., bitwise negation (one's complement), conjunction, disjunction and exclusive-or. These functions are defined quite naturally using the Cartesian product binder; for example, bitwise conjunction is defined by:

|- !v w. v && w = FCP i. v ' i /\ w ' i .

There is also a collection of word reduction operations, which reduce bit vectors to 1-bit words, e.g.,

$$ \mathrm{reduce\_and}(x)\;'\;0 = \bigwedge_{i=0}^{n-1} x_i\,. $$

The functions word_lsb, word_msb and word_bit(i) give the bit value of a word at positions $0$, $n-1$ and $i$ respectively. Four operations are provided for selecting bit fields, or sub-words: word_bits (--), word_signed_bits (---), word_slice ('') and word_extract (><). For example, word_bits 4 1 will select four bits starting from bit position

  1. The slice function is an in-place variant (it zeroes bits outside of the bit range) and the extract function combines word_bits with a word cast (w2w). The operation word_signed_bits is similar to word_bits, except that it sign-extends the bit field.

The bit_field_insert operation inserts a bit field. For example,

bit_field_insert 5 2 a b

is word b with bits 5–2 replaced by bits 3–0 of a.

A word's bit ordering can be flipped over with word_reverse, i.e., bit zero is swapped with bit $n-1$ and so forth.

The function word_modify:(num -> bool -> bool) -> $\worda \to \worda$ changes a word by applying a map at each bit position. This operation provides a very flexible and convenient mechanism for manipulating words, e.g.,

word_modify (\i b. if EVEN i then ~b else b) w

negates the bits of w that are in even positions. Of course, the binder FCP also provides a very general means to represent words using a predicate; e.g., $FCP ODD represents a word where all the odd bits are set.

Shifts. Six types of shifts are provided: logical shift left/right (<< and >>>), arithmetic shift right (>>), rotate left/right (#<< and #>>) and rotate right extended by 1 place (word_rrx). These shifts are illustrated in Figure 5.3.8.3 and are defined in a similar manner to the other bit field operations. For example, rotating right is defined by:

|- !w n. w #>> x = FCP i. w ' (i + x) MOD dimindex (:'a) .

Rotating left by $x$ places is defined as rotating right by $n - x \bmod n$ places.

Logical shift left Logical shift right
(a) Logical shift left: w = v << x. (b) Logical shift right: w = v >>> x.
Arithmetic shift right Rotate right
(c) Arithmetic shift right: w = v >> x. (d) Rotate right: w = v #>> x.
Rotate right extended by 1 place
(e) Rotate right extended by 1 place: (d,w) = word_rrx (c,v).
Shift operations.

Arithmetic and orderings. The arithmetic operations are: addition, subtraction, unary minus (two's complement), logarithm (base-2), multiplication, modulus and division (signed and unsigned). These operations are defined with respect to the natural numbers. For example, word addition is defined by:

|- !v w. v + w = n2w (w2n v + w2n w)

The + on the left-hand side is word addition and on the right it is natural number addition.

All of the standard word orderings are provided, with signed and unsigned versions of $<$, $\leq$, $>$ and $\geq$. The unsigned versions are suffixed with a plus; for example, <+ is unsigned “less than”.

Constants. The word theory also defines a few word constants:

ConstantValueBinary
word_T or UINT_MAXw$2^l - 1$$11\cdots 11$
word_L or INT_MINw$2^{l-1}$$10\cdots 00$
word_H or INT_MAXw$2^{l-1} - 1$$01\cdots 11$

List of bit vector operations. A list of operations is provided in the table below.

OperationSymbolTypeDescription
n2w$\mathit{num}\to\worda$Map from a natural number
w2n$\worda\to\mathit{num}$Map to a natural number
w2w$\worda\to\wordb$Map word-to-word (unsigned)
sw2sw$\worda\to\wordb$Map word-to-word (signed)
w2l$\mathit{num}\to\worda\to\mathit{num}\,\mathit{list}$Map word to digit list
l2w$\mathit{num}\to\mathit{num}\,\mathit{list}\to\worda$Map digit list to word
w2s$\mathit{num}\to(\mathit{num}\to\mathit{char})\to\worda\to\mathit{string}$Map word to string
s2w$\mathit{num}\to(\mathit{char}\to\mathit{num})\to\mathit{string}\to\worda$Map string to word
word_len$\worda\to\mathit{num}$The word length
word_lsb$\worda\to\mathit{bool}$The least significant bit
word_msb$\worda\to\mathit{bool}$The most significant bit
word_bit$\mathit{num}\to\worda\to\mathit{bool}$Test bit position
word_bits--$\mathit{num}\to\mathit{num}\to\worda\to\worda$Select a bit field
word_signed_bits---$\mathit{num}\to\mathit{num}\to\worda\to\worda$Sign-extend selected bit field
word_slice''$\mathit{num}\to\mathit{num}\to\worda\to\worda$Set bits outside field to zero
word_extract><$\mathit{num}\to\mathit{num}\to\worda\to\wordb$Extract (cast) a bit field
word_reverse$\worda\to\worda$Reverse the bit order
bit_field_insert$\mathit{num}\to\mathit{num}\to\worda\to\wordb\to\wordb$Insert a bit field
word_modify$(\mathit{num}\to\mathit{bool}\to\mathit{bool})\to\worda\to\worda$Apply a function to each bit
word_join$\worda\to\wordb\to\fcp{\mathit{bool}}{\alpha+\beta}$Join words
word_concat@@$\worda\to\wordb\to\wordc$Concatenate words
concat_word_list$\worda\,\mathit{list}\to\wordb$Concatenate list of words
word_replicate$\mathit{num}\to\worda\to\wordb$Replicate word
word_or||$\worda\to\worda\to\worda$Bitwise disjunction
word_xor??$\worda\to\worda\to\worda$Bitwise exclusive-or
word_and&&$\worda\to\worda\to\worda$Bitwise conjunction
word_nor~||$\worda\to\worda\to\worda$Bitwise NOR
word_xnor~??$\worda\to\worda\to\worda$Bitwise XNOR
word_nand~&&$\worda\to\worda\to\worda$Bitwise NAND
word_reduce$(\mathit{bool}\to\mathit{bool}\to\mathit{bool})\to\worda\to\fcp{\mathit{bool}}{1}$Word reduction
reduce_or$\worda\to\fcp{\mathit{bool}}{1}$Disjunction reduction
reduce_xor$\worda\to\fcp{\mathit{bool}}{1}$Exclusive-or reduction
reduce_and$\worda\to\fcp{\mathit{bool}}{1}$Conjunction reduction
reduce_nor$\worda\to\fcp{\mathit{bool}}{1}$NOR reduction
reduce_xnor$\worda\to\fcp{\mathit{bool}}{1}$XNOR reduction
reduce_nand$\worda\to\fcp{\mathit{bool}}{1}$NAND reduction
word_1comp~$\worda\to\worda$One's complement
word_2comp-$\worda\to\worda$Two's complement
word_add+$\worda\to\worda\to\worda$Addition
word_sub-$\worda\to\worda\to\worda$Subtraction
word_mul*$\worda\to\worda\to\worda$Multiplication
word_div//$\worda\to\worda\to\worda$Division (unsigned)
word_sdiv/$\worda\to\worda\to\worda$Division (signed)
word_mod$\worda\to\worda\to\worda$Modulus
word_log2$\worda\to\worda$Logarithm base-2
word_lsl<<$\worda\to\mathit{num}\to\worda$Logical shift left
word_lsr>>>$\worda\to\mathit{num}\to\worda$Logical shift right
word_asr>>$\worda\to\mathit{num}\to\worda$Arithmetic shift right
word_ror#>>$\worda\to\mathit{num}\to\worda$Rotate right
word_rol#<<$\worda\to\mathit{num}\to\worda$Rotate left
word_rrx$\mathit{bool}\#\worda\to\mathit{bool}\#\worda$Rotate right extended by 1 place
word_lt<$\worda\to\worda\to\mathit{bool}$Signed “less than”
word_le<=$\worda\to\worda\to\mathit{bool}$Signed “less than or equal”
word_gt>$\worda\to\worda\to\mathit{bool}$Signed “greater than”
word_ge>=$\worda\to\worda\to\mathit{bool}$Signed “greater than or equal”
word_lo<+$\worda\to\worda\to\mathit{bool}$Unsigned “less than”
word_ls<=+$\worda\to\worda\to\mathit{bool}$Unsigned “less than or equal”
word_hi>+$\worda\to\worda\to\mathit{bool}$Unsigned “greater than”
word_hs>=+$\worda\to\worda\to\mathit{bool}$Unsigned “greater than or equal”

Sequences

HOL provides theories for various kinds of sequences: finite lists, lazy lists, paths, and finite strings.

Lists

HOL lists are inductively defined finite sequences where each element in a list has the same type. The theory list introduces the unary type operator $\alpha\;\konst{list}$ by a type definition and a standard collection of list processing functions are defined. The primitive constructors NIL and CONS

   NIL  : 'a list
   CONS : 'a -> 'a list -> 'a list

are used to build lists and have been defined from the representing type for lists. The HOL parser has been specially modified to parse the expression [] into NIL, to parse the expression h::t into CONS h t, and to parse the expression [$t_1$;$t_2$;…;$t_n$] into CONS $t_1$(CONS$t_2$ $\cdots$(CONS$t_n$NIL)$\cdots$). The HOL printer reverses these transformations.

Based on the inductive characterization of the type, the following fundamental theorems about lists are proved and stored in the theory list.

list_Axiom
  ⊢ ∀f0 f1. ∃fn.
       fn [] = f0 ∧ ∀a0 a1. fn (a0::a1) = f1 a0 a1 (fn a1)
list_INDUCT
  ⊢ ∀P. P [] ∧ (∀t. P t ⇒ ∀h. P (h::t)) ⇒ ∀l. P l
list_CASES
  ⊢ ∀l. l = [] ∨ ∃h t. l = h::t
CONS_11
  ⊢ ∀a0 a1 a0' a1'. a0::a1 = a0'::a1' ⇔ a0 = a0' ∧ a1 = a1'
NOT_NIL_CONS
  ⊢ ∀a1 a0. [] ≠ a0::a1
NOT_CONS_NIL
  ⊢ ∀a1 a0. a0::a1 ≠ []

The theorem list_Axiom shown above is analogous to the primitive recursion theorem on the natural numbers discussed above in Section 5.3.1.3. It states the validity of primitive recursive definitions on lists, and can be used to justify any such definition. The ML function new_recursive_definition uses this theorem to do automatic proofs of the existence of primitive recursive functions on lists and then make constant specifications to introduce constants that denote such functions.

The induction theorem for lists, list_INDUCT, provides the main proof tool used to reason about operations that manipulate lists. The theorem list_CASES is used to perform case analysis on whether a list is empty or not.

The theorem CONS_11 shows that CONS is injective; the theorems NOT_NIL_CONS and NOT_CONS_NIL show that NIL and CONS are distinct, i.e., cannot give rise to the same structure. Together, these three theorems are used for equational reasoning about lists.

The predicate NULL and the selectors HD and TL are defined in the theory list by

NULL
  ⊢ NULL [] ∧ ∀h t. ¬NULL (h::t)
HD
  ⊢ ∀h t. HD (h::t) = h
TL_DEF
  ⊢ TL [] = [] ∧ ∀h t. TL (h::t) = t

The nil-clause for the TL constant is included to make the function total, but does represent a case that often needs to be excluded. For example:

LIST_NOT_NIL
  ⊢ ∀ls. ls ≠ [] ⇔ ls = HD ls::TL ls

The following functions on lists are also defined in the theory list.

Case expressions. Compound HOL expressions that branch based on whether a term is an empty or non-empty list have the surface syntax (roughly borrowed from ML)

   case e1
    of [] => e2
     | (h::t) => e3

Such an expression is translated to $\mathtt{list\_CASE}\;e_1\;e_2\;(\lambda h\;t.\;e_3)$ where the constant list_CASE is defined as follows:

list_case_def
  ⊢ (∀v f. list_CASE [] v f = v) ∧
     ∀a0 a1 v f. list_CASE (a0::a1) v f = f a0 a1

List membership. Membership in a list, written using the MEM syntax, is characterised as follows:

MEM
  ⊢ (∀x. MEM x [] ⇔ F) ∧ ∀x h t. MEM x (h::t) ⇔ x = h ∨ MEM x t

Concatenation of lists.

Binary list concatenation (APPEND) may also be denoted by the infix operator ++; thus the expression L1 ++ L2 is translated into APPEND L1 L2. The concatenation of a list of lists into a list is achieved by FLAT. The special case where a single element is appended to the end of a list (the “opposite” of CONS, which adds elements to the front of a list), is implemented by SNOC.

APPEND
  ⊢ (∀l. [] ⧺ l = l) ∧ ∀l1 l2 h. h::l1 ⧺ l2 = h::(l1 ⧺ l2)
FLAT
  ⊢ FLAT [] = [] ∧ ∀h t. FLAT (h::t) = h ⧺ FLAT t
SNOC
  ⊢ (∀x. SNOC x [] = [x]) ∧
     ∀x x' l. SNOC x (x'::l) = x'::SNOC x l

Numbers and lists.

The length (LENGTH) and size (list_size) of a list are related notions. The size of a list takes account of the size of each element of the list (given by parameter $f:\alpha\to\konst{num}$), while the length of the list ignores the size of each list element. The alternate length definition (LEN) is tail-recursive. Numbers can also be used to index into lists, extracting the element at the specified position.

LENGTH
  ⊢ LENGTH [] = 0 ∧ ∀h t. LENGTH (h::t) = SUC (LENGTH t)
LEN_DEF
  ⊢ (∀n. LEN [] n = n) ∧ ∀h t n. LEN (h::t) n = LEN t (n + 1)
list_size_def
  ⊢ (∀f. list_size f [] = 0) ∧
     ∀f a0 a1. list_size f (a0::a1) = 1 + (f a0 + list_size f a1)
EL
  ⊢ (∀l. l❲0❳ = HD l) ∧ ∀l n. l❲SUC n❳ = (TL l)❲n❳

Note that the extraction of the $n$th element (as described in the theorem EL) of a list starts its indexing from 0. If the length of the list $\ell$ is less than or equal to n, the result of `ℓ❲n❳` is unspecified. The special syntax with the brackets hides the underlying constant, which is EL of type :num -> 'a list -> 'a. It is legal to use the constant explicitly, writing EL n ℓ.

The GENLIST constant can be used to generate a list of a particular size, where the value of each element is independently determined by reference to a function that takes natural numbers (the set $\{0\dots n-1\}$) to element values:

GENLIST
  ⊢ (∀f. GENLIST f 0 = []) ∧
     ∀f n. GENLIST f (SUC n) = SNOC (f n) (GENLIST f n)
EL_GENLIST
  ⊢ ∀f n x. x < n ⇒ (GENLIST f n)❲x❳ = f x

Working with SNOC, and thus the definition above, can occasionally be awkward, so a characterisation of GENLIST's SUC clause in terms of CONS can also be useful:

GENLIST_CONS
  ⊢ GENLIST f (SUC n) = f 0::GENLIST (f ∘ SUC) n

For more on the “indexed” treatment of lists, see Section 5.4.1.2 below.

Mapping functions over lists.

There are functions for mapping a function $f:\alpha\to\beta$ over a single list (MAP), a “partial” function $f:\alpha\to\beta\,\mathsf{option}$ over a list (mapPartial), or a function $f:\alpha\to\beta\to\gamma$ over two lists (MAP2):

MAP
  ⊢ (∀f. MAP f [] = []) ∧ ∀f h t. MAP f (h::t) = f h::MAP f t
mapPartial_def
  ⊢ (∀f. mapPartial f [] = []) ∧
     ∀f x xs.
       mapPartial f (x::xs) =
       case f x of
         NONE => mapPartial f xs
       | SOME y => y::mapPartial f xs
MAP2_DEF
  ⊢ (∀t2 t1 h2 h1 f.
        MAP2 f (h1::t1) (h2::t2) = f h1 h2::MAP2 f t1 t2) ∧
     (∀y f. MAP2 f [] y = []) ∧ ∀v5 v4 f. MAP2 f (v4::v5) [] = []

If passed lists of unequal length, MAP2 returns a list of length equal to that of the shorter list.

Predicates over lists.

Predicates can be applied to lists in a universal sense (the predicate must hold of every element in the list) or an existential sense (the predicate must hold of some element in the list). This functionality is supported by EVERY and EXISTS, respectively. The elimination of all elements in list not satisfying a given predicate is performed by FILTER.

EVERY_DEF
  ⊢ (∀P. EVERY P [] ⇔ T) ∧
     ∀P h t. EVERY P (h::t) ⇔ P h ∧ EVERY P t
EXISTS_DEF
  ⊢ (∀P. EXISTS P [] ⇔ F) ∧
     ∀P h t. EXISTS P (h::t) ⇔ P h ∨ EXISTS P t
FILTER
  ⊢ (∀P. FILTER P [] = []) ∧
     ∀P h t.
       FILTER P (h::t) =
       if P h then h::FILTER P t else FILTER P t
ALL_DISTINCT
  ⊢ (ALL_DISTINCT [] ⇔ T) ∧
     ∀h t. ALL_DISTINCT (h::t) ⇔ ¬MEM h t ∧ ALL_DISTINCT t

The predicate ALL_DISTINCT holds on a list just in case no element in the list is equal to any other. A list can have its duplicates removed through the use of the nub constant:

nub_def
  ⊢ nub [] = [] ∧
     ∀x l. nub (x::l) = if MEM x l then nub l else x::nub l

Relations over lists. A binary relation on elements can be “lifted” to a relation on lists of such elements with the LIST_REL constant:

LIST_REL_def
  ⊢ (LIST_REL R [] [] ⇔ T) ∧ (LIST_REL R (a::as) [] ⇔ F) ∧
     (LIST_REL R [] (b::bs) ⇔ F) ∧
     (LIST_REL R (a::as) (b::bs) ⇔ R a b ∧ LIST_REL R as bs)

This can be viewed as an application of EVERY:

LIST_REL_EVERY_ZIP
  ⊢ ∀R l1 l2.
       LIST_REL R l1 l2 ⇔
       LENGTH l1 = LENGTH l2 ∧ EVERY (UNCURRY R) (ZIP (l1,l2))

Acknowledging this view, the system overloads the name EVERY2 to map to the same constant.

> “EVERY2 (λm n. EVEN (m + n)) [1;2;3] [3;4;5]”;
val it = “LIST_REL (λm n. EVEN (m + n)) [1; 2; 3] [3; 4; 5]”:
   term

Some theorems in listTheory have names that reflect this.

Equally, LIST_REL can be seen as a test that checks the relation at all relevant indices:

LIST_REL_EL_EQN
  ⊢ ∀R l1 l2.
       LIST_REL R l1 l2 ⇔
       LENGTH l1 = LENGTH l2 ∧ ∀n. n < LENGTH l1 ⇒ R l1❲n❳ l2❲n❳

Finally, there is a natural induction principle for this constant (as per Section 7.7.1, the tactic ``Induct_on `LIST_REL``` applies it):

LIST_REL_strongind
  ⊢ ∀R LIST_REL'.
       LIST_REL' [] [] ∧
       (∀h1 h2 t1 t2.
          R h1 h2 ∧ LIST_REL R t1 t2 ∧ LIST_REL' t1 t2 ⇒
          LIST_REL' (h1::t1) (h2::t2)) ⇒
       ∀a0 a1. LIST_REL R a0 a1 ⇒ LIST_REL' a0 a1

Folding.

Applying a binary function $f:\alpha\to\beta\to\beta$ pairwise through a list and accumulating the result is known as folding. At times, it is necessary to do this operation from left-to-right (FOLDL), and at others the right-to-left direction (FOLDR) is required.

FOLDL
  ⊢ (∀f e. FOLDL f e [] = e) ∧
     ∀f e x l. FOLDL f e (x::l) = FOLDL f (f e x) l
FOLDR
  ⊢ (∀f e. FOLDR f e [] = e) ∧
     ∀f e x l. FOLDR f e (x::l) = f x (FOLDR f e l)

List reversal. The reversal of a list (REVERSE) and its tail recursive counterpart REV are defined in list.

   REVERSE_DEF
     |- (REVERSE [] = []) /\
        (!h t. REVERSE (h::t) = REVERSE t ++ [h])
   REV_DEF
     |- (!acc. REV [] acc = acc) /\
        (!h t acc. REV (h::t) acc = REV t (h::acc))

Conversion to sets. Lists can be converted to sets with the LIST_TO_SET constant, which is overloaded to the prettier name set. The definition is made by primitive recursion in listTheory:

> listTheory.LIST_TO_SET;
val it = ⊢ set [] = ∅ ∧ set (h::t) = h INSERT set t: thm

Note that MEM is an overloaded form of syntax such that MEM x l is actually a pretty-printing of the underlying term x ∈ set l.

Further support for translating between different kinds of collections may be found in the container theory.

Pairs and lists. Two lists of equal length may be component-wise paired by the ZIP operation. As with MAP2, the result of zipping lists of unequal lengths is a list whose length is that of the shorter argument. The inverse operation, UNZIP, translates a list of pairs into a pair of lists.

ZIP_def
  ⊢ (∀l2. ZIP ([],l2) = []) ∧ (∀l1. ZIP (l1,[]) = []) ∧
     ∀x1 l1 x2 l2. ZIP (x1::l1,x2::l2) = (x1,x2)::ZIP (l1,l2)
UNZIP_THM
  ⊢ UNZIP [] = ([],[]) ∧
     UNZIP ((x,y)::t) = (let (L1,L2) = UNZIP t in (x::L1,y::L2))

Alternate access.

Lists are essentially treated in a stack-like manner. However, at times it is convenient to access the last element (LAST) of a non-empty list directly. The last element of a non-empty list is dropped by FRONT.

LAST_DEF
  ⊢ ∀h t. LAST (h::t) = if t = [] then h else LAST t
FRONT_DEF
  ⊢ FRONT [] = [] ∧
     ∀h t. FRONT (h::t) = if t = [] then [] else h::FRONT t
APPEND_FRONT_LAST
  ⊢ ∀l. l ≠ [] ⇒ FRONT l ⧺ [LAST l] = l

Joining the front part and the last element of a non-empty list yields the original list. Both LAST and FRONT are unspecified on empty lists.

Prefix checking. The relation capturing whether a list $\ell_1$ is a prefix of $\ell_2$ (isPREFIX) can be defined by recursion. The infix symbols <<= (ASCII) and $\preccurlyeq$ (U+227C) can also be used as notation for this partial order.

isPREFIX_THM
  ⊢ ([] ≼ l ⇔ T) ∧ (h::t ≼ [] ⇔ F) ∧
     (h1::t1 ≼ h2::t2 ⇔ h1 = h2 ∧ t1 ≼ t2)

The above theorem states that: the empty list is a prefix of any other list (clause 1); that no non-empty list is a prefix of the empty list (clause 2); and that a non-empty list is a prefix of another non-empty list if the first elements of the lists are the same, and if the tail of the first is a prefix of the tail of the second.

For a complete list of available theorems in list, see the REFERENCE manual. Further development of list theory can be found in rich_list.

List permutations and sorting

The sorting theory defines a notion of two lists being permutations of each other, then defines a general notion of sorting, then shows that Quicksort is a sorting function. The mergesort theory defines Merge sort and shows that it is a stable sorting function.

List permutation. Two lists are in permutation if they have exactly the same members, and each member has the same number of occurrences in both lists. One definition (PERM) that captures this relationship is the following:

PERM_DEF
  ⊢ ∀L1 L2. PERM L1 L2 ⇔ ∀x. FILTER ($= x) L1 = FILTER ($= x) L2
PERM_IND
  ⊢ ∀P. P [] [] ∧ (∀x l1 l2. P l1 l2 ⇒ P (x::l1) (x::l2)) ∧
         (∀x y l1 l2. P l1 l2 ⇒ P (x::y::l1) (y::x::l2)) ∧
         (∀l1 l2 l3. P l1 l2 ∧ P l2 l3 ⇒ P l1 l3) ⇒
         ∀l1 l2. PERM l1 l2 ⇒ P l1 l2

A derived induction theorem (PERM_IND) is very useful in proofs about permutations.

Sorting. A list is $R$-sorted if $R$ holds pairwise through the list. This notion (SORTED) is captured by a recursive definition. Then a function of type

   ('a -> 'a -> bool) -> 'a list -> 'a list

is a sorting function (SORTS) with respect to $R$ if it delivers a permutation of its input, and the result is $R$-sorted.

SORTED_DEF
  ⊢ (∀R. SORTED R [] ⇔ T) ∧ (∀x R. SORTED R [x] ⇔ T) ∧
     ∀y x rst R. SORTED R (x::y::rst) ⇔ R x y ∧ SORTED R (y::rst)
SORTS_DEF
  ⊢ ∀f R. SORTS f R ⇔ ∀l. PERM l (f R l) ∧ SORTED R (f R l)

Quicksort is defined in the usual functional programming style, and it is indeed a sorting function, provided $R$ is a transitive and total relation.

QSORT_DEF
  ⊢ (∀ord. QSORT ord [] = []) ∧
     ∀t ord h.
       QSORT ord (h::t) =
       (let
          (l1,l2) = PARTITION (λy. ord y h) t
        in
          QSORT ord l1 ⧺ [h] ⧺ QSORT ord l2)
QSORT_SORTS
  ⊢ ∀R. transitive R ∧ total R ⇒ SORTS QSORT R

The notion of $R$ holding pairwise through a list can be expressed using the predicate adjacent, where $\mathtt{adjacent}\;\ell\;a\;b$ holds if values $a$ and $b$ appear together (in that order) in list $\ell$. Then, we have

SORTED_adjacent
  ⊢ SORTED R L ⇔ adjacent L ⊆ᵣ R

where the $\subseteq\subr$ relation is the notion of relation-subset (see Section 5.5.3). There are a number of other theorems in listTheory about adjacency, including for example:

adjacent_REVERSE
  ⊢ ∀xs a b. adjacent (REVERSE xs) a b ⇔ adjacent xs b a
adjacent_MAP
  ⊢ ∀xs a b f.
       adjacent (MAP f xs) a b ⇔ ∃x y. adjacent xs x y ∧ a = f x ∧ b = f y

Indexed lists

As mentioned earlier, lists can be indexed with the constant EL, viewing lists as partial functions from natural numbers (starting at 0!) into the element type. The definition is given by primitive recursion over the index argument, in theorem EL:

EL
  ⊢ (∀l. l❲0❳ = HD l) ∧ ∀l n. l❲SUC n❳ = (TL l)❲n❳

The term underlying the pretty presentation $\ell\lbrbrak n\rbrbrak$ is $\mathtt{EL}\;n\;\ell$; and both forms can be used when writing terms. If desired, the pretty-printing with the “array-subscript” notation can be turned off by the invocation

   val _ = clear_overloads_on "fEL"

Note that because of the use of HD and TL, the value of $\ell\lbrbrak n\rbrbrak$ is unspecified when $n \geq \mathtt{LENGTH}\;\ell$. Subsequently, many theorems involving EL have preconditions to preclude this possibility.

For example, these theorems describing the relationship between EL, MAP and MEM:

EL_MAP
  ⊢ ∀n l. n < LENGTH l ⇒ ∀f. (MAP f l)❲n❳ = f l❲n❳
MEM_EL
  ⊢ ∀l x. MEM x l ⇔ ∃n. n < LENGTH l ∧ x = l❲n❳

It is occasionally useful to be able to update lists at particular positions, viewing them as similar to programming language arrays. The relevant constant is LUPDATE, where the term $\mathtt{LUPDATE}\;e\;n\;\ell$ has the same value as list $\ell$, except that the $n$-th element of the list is equal to $e$. The definition illustrates the pretty syntax for the above ($\ell\lbrbrak n\mapsto e\rbrbrak$), and is given in three clauses:

LUPDATE_def
  ⊢ (∀e n. []❲n ↦ e❳ = []) ∧ (∀e x l. (x::l)❲0 ↦ e❳ = e::l) ∧
     ∀e n x l. (x::l)❲SUC n ↦ e❳ = x::l❲n ↦ e❳

The definition implies that attempting to update a list at an index beyond the end of the list returns the input list unchanged.

The basic characterisation of the link between EL and LUPDATE is

EL_LUPDATE
  ⊢ ∀ys x i k. ys❲k ↦ x❳❲i❳ = if i = k ∧ k < LENGTH ys then x else ys❲i❳

The pretty syntax supports chained or nested LUPDATE applications using a list-like notation:

> “LUPDATE v1 k1 (LUPDATE v2 k2 ℓ)”;
<<HOL message: inventing new type variable names: 'a>>
val it = “ℓ❲k1 ↦ v1; k2 ↦ v2❳”: term

The indexedLists theory. The indexedLists theory defines a number of extra constants that are “aware” of lists as indexed values. Some of these constants are:

   delN      : num -> 'a list -> 'a list
   findi     : 'a -> 'a list -> num
   LIST_RELi : (num -> 'a -> 'b -> bool) -> 'a list -> 'b list -> bool
   MAPi      : (num -> 'a -> 'b) -> 'a list -> 'b list

The findi constant is such that $\mathtt{findi}\;e\;\ell$ returns the first index of element $e$ within list $\ell$, or a number equal to $\ell$'s length, if $e$ is not present. The definition is by recursion over the structure of the input list:

findi_def
  ⊢ (∀x. findi x [] = 0) ∧
     ∀x h t. findi x (h::t) = if x = h then 0 else 1 + findi x t

The delN constant is used to remove the $n$-th element from a list. It is also defined by recursion over the structure of the input list:

delN_def
  ⊢ (∀i. delN i [] = []) ∧
     ∀i h t. delN i (h::t) = if i = 0 then t else h::delN (i − 1) t

The higher-order MAPi function exemplifies another set of constants within the indexedLists theory: its function parameter, which works on elements of the list argument is given access to the index of the list element as well as its value. A simple example use might be to generate a numbered version of a list, using the term $\mathtt{MAPi}\;(\lambda i\;e.\;(i,e))$. If this term were applied to the list [a;c;d] the resulting value would be [(0,a);(1,c);(2,d)].

An example theorem about MAPi relates it to MEM:

MEM_MAPi
  ⊢ ∀x f l. MEM x (MAPi f l) ⇔ ∃n. n < LENGTH l ∧ x = f n l❲n❳

Possibly infinite sequences (llist)

The theory llist contains the definition of a type of possibly infinite sequences. This type is similar to the “lazy lists” of programming languages like Haskell, hence the name of the theory. The llist theory has a number of constants that are analogous to constants in the theory of finite lists. The llist versions of these constants have the same names, but with a capital 'L' prepended. Thus, some of the core constants in this theory are:

   LNIL  : 'a llist
   LCONS : 'a -> 'a llist -> 'a llist
   LHD   : 'a llist -> 'a option
   LTL   : 'a llist -> 'a llist option

The LHD and LTL constants return NONE when applied to the empty sequence, LNIL. This use of an option type is another way of modelling the essential partiality of these constants. (In the theory of lists, the analogous HD and TL functions simply have unspecified values when applied to empty lists.)

The type llist is not inductive, and there is no primitive recursion theorem supporting the definition of functions that have domains of type llist. Rather, llist is a coinductive type, and has an axiom that justifies the definition of (co-)recursive functions that map into the llist type:

   llist_Axiom
     ⊢ ∀f. ∃g.
          (∀x. LHD (g x) = OPTION_MAP SND (f x)) ∧
          ∀x. LTL (g x) = OPTION_MAP (g ∘ FST) (f x)

An equivalent form of the above is

llist_Axiom_1
  ⊢ ∀f. ∃g. ∀x. g x = case f x of NONE => [||] | SOME (a,b) => b:::g a

Other constants in the theory llist include LMAP, LFINITE, LNTH, LTAKE, LDROP, and LFILTER. Their types are

   LMAP    : ('a -> 'b) -> 'a llist -> 'b llist
   LFINITE : 'a llist -> bool
   LNTH    : num -> 'a llist -> 'a option
   LTAKE   : num -> 'a llist -> 'a list option
   LDROP   : num -> 'a llist -> 'a llist option
   LFILTER : ('a -> bool) -> 'a llist -> 'a llist

They are characterised by the following theorems

LMAP
  ⊢ (∀f. LMAP f [||] = [||]) ∧ ∀f h t. LMAP f (h:::t) = f h:::LMAP f t
LFINITE_THM
  ⊢ (LFINITE [||] ⇔ T) ∧ ∀h t. LFINITE (h:::t) ⇔ LFINITE t
LNTH_THM
  ⊢ (∀n. LNTH n [||] = NONE) ∧ (∀h t. LNTH 0 (h:::t) = SOME h) ∧
     ∀n h t. LNTH (SUC n) (h:::t) = LNTH n t
LTAKE_THM
  ⊢ (∀l. LTAKE 0 l = SOME []) ∧ (∀n. LTAKE (SUC n) [||] = NONE) ∧
     ∀n h t. LTAKE (SUC n) (h:::t) = OPTION_MAP (CONS h) (LTAKE n t)
LDROP_THM
  ⊢ (∀ll. LDROP 0 ll = SOME ll) ∧ (∀n. LDROP (SUC n) [||] = NONE) ∧
     ∀n h t. LDROP (SUC n) (h:::t) = LDROP n t
LFILTER_THM
  ⊢ (∀P. LFILTER P [||] = [||]) ∧
     ∀P h t. LFILTER P (h:::t) = if P h then h:::LFILTER P t else LFILTER P t

Concatenation. Two lazy lists may be concatenated by LAPPEND (written below using its infix Unicode form ++$_l$). If the first lazy list is infinite, elements of the second are inaccessible in the result. A lazy list of lazy lists can be flattened to a lazy list by LFLATTEN.

LAPPEND
  ⊢ (∀x. [||] ++ₗ x = x) ∧ ∀h t x. h:::t ++ₗ x = h:::(t ++ₗ x)
LFLATTEN_THM
  ⊢ LFLATTEN [||] = [||] ∧ (∀tl. LFLATTEN ([||]:::t) = LFLATTEN t) ∧
     ∀h t tl. LFLATTEN ((h:::t):::tl) = h:::LFLATTEN (t:::tl)

Lists and lazy lists. Mapping back and forth from lists to lazy lists is accomplished by fromList and toList:

fromList_def
  ⊢ fromList [] = [||] ∧ ∀h t. fromList (h::t) = h:::fromList t
toList_THM
  ⊢ toList [||] = SOME [] ∧
     ∀h t. toList (h:::t) = OPTION_MAP (CONS h) (toList t)

Note that toList ll = NONE when ll is infinite.

Proof principles. Finally, there are two very important proof principles for proving that two llist values are equal. The first states that two sequences are equal if they return the same prefixes of length $n$ for all possible values of $n$:

LTAKE_EQ
  ⊢ ∀ll1 ll2. ll1 = ll2 ⇔ ∀n. LTAKE n ll1 = LTAKE n ll2

This theorem is subsequently used to derive the bisimulation principle:

LLIST_BISIMULATION
  ⊢ ∀ll1 ll2.
       ll1 = ll2 ⇔
       ∃R. R ll1 ll2 ∧
           ∀ll3 ll4.
             R ll3 ll4 ⇒
             ll3 = [||] ∧ ll4 = [||] ∨
             LHD ll3 = LHD ll4 ∧ R (THE (LTL ll3)) (THE (LTL ll4))

The principle of bisimulation states that two llist values $l_1$ and $l_2$ are equal if (and only if) it is possible to find a relation $R$ such that

  • $R$ relates the two values, i.e., $R\;l_1\;l_2$; and
  • if $R$ holds of any two values $l_3$ and $l_4$, then either
    • both $l_3$ and $l_4$ are empty; or
    • the head elements of $l_3$ and $l_4$ are the same, and the tails of those two values are again related by $R$

Of course, a possible $R$ would be equality itself, but the strength of this theorem is that other, more convenient relations can also be used.

Labelled paths (path)

The theory path defines a binary type operator $(\alpha,\beta)\,\mathtt{path}$, which stands for possibly infinite paths of the following form

$$ \alpha_1 \stackrel{\beta_1}{\longrightarrow} \alpha_2 \stackrel{\beta_2}{\longrightarrow} \alpha_3 \stackrel{\beta_3}{\longrightarrow} \cdots \alpha_n \stackrel{\beta_n}{\longrightarrow} \alpha_{n+1} \stackrel{\beta_{n+1}}{\longrightarrow} \cdots $$

The path type is thus an appropriate model for reduction sequences, where the $\alpha$ parameter corresponds to “states”, and the $\beta$ parameter corresponds to the labels on the arrows.

The model of $(\alpha,\beta)\,\mathtt{path}$ is $\alpha \times ((\alpha\times\beta)\,\mathtt{llist})$. The type of paths has two constructors:

   stopped_at : 'a -> ('a,'b) path
   pcons      : 'a -> 'b -> ('a,'b) path -> ('a,'b) path

The stopped_at constructor returns a path containing just one state, and no transitions. (Thus, the reduction sequence has “stopped at” this state.) The pcons constructor takes a state, a label, and a path, and returns a path which is now headed by the state argument, and which moves from that state via the label argument to the path. Graphically, $\mathtt{pcons}\;x\;l\;p$ is equal to

$$ x \stackrel{l}{\longrightarrow} \underbrace{p_1 \stackrel{l_1}{\longrightarrow} p_2 \stackrel{l_2}{\longrightarrow} \cdots\quad}_p $$

Other constants defined in theory path include

   finite  : ('a,'b) path -> bool
   first   : ('a,'b) path -> 'a
   labels  : ('a,'b) path -> 'b llist
   last    : ('a,'b) path -> 'a
   length  : ('a,'b) path -> num option
   okpath  : ('a -> 'b -> 'a -> bool) -> ('a,'b) path -> bool
   pconcat : ('a,'b) path -> 'b -> ('a,'b) path -> ('a,'b) path
   pmap    : ('a -> 'c) -> ('b -> 'd) -> ('a,'b)path -> ('c,'d)path

The first function returns the first element of a path. There always is such an element, and the defining equations are

first_thm
  ⊢ (∀x. first (stopped_at x) = x) ∧ ∀x r p. first (pcons x r p) = x

On the other hand, the last function does not always have a well-specified value, though it still has nice characterising equations:

last_thm
  ⊢ (∀x. last (stopped_at x) = x) ∧ ∀x r p. last (pcons x r p) = last p

The theorem for finite has a similar feel, but has a definite value (F, or false) on infinite paths, whereas the value of last on such paths is unspecified:

finite_thm
  ⊢ (∀x. finite (stopped_at x) ⇔ T) ∧
     ∀x r p. finite (pcons x r p) ⇔ finite p

The function pconcat concatenates two paths, linking them with a provided label. If the first path is infinite, then the result is equal to that first path. The defining equation is

pconcat_thm
  ⊢ (∀x lab p2. pconcat (stopped_at x) lab p2 = pcons x lab p2) ∧
     ∀x r p lab p2.
       pconcat (pcons x r p) lab p2 = pcons x r (pconcat p lab p2)

These equations are true even when the first argument to pconcat is an infinite path.

The okpath predicate tests whether or not a path is a valid transition given a ternary transition relation. Its characterising theorem is

okpath_thm
  ⊢ ∀R. (∀x. okpath R (stopped_at x)) ∧
         ∀x r p. okpath R (pcons x r p) ⇔ R x r (first p) ∧ okpath R p

There is also an induction principle that simplifies reasoning about finite $R$-paths:

finite_okpath_ind
  ⊢ ∀R. (∀x. P (stopped_at x)) ∧
         (∀x r p.
            okpath R p ∧ finite p ∧ R x r (first p) ∧ P p ⇒ P (pcons x r p)) ⇒
         ∀sigma. okpath R sigma ∧ finite sigma ⇒ P sigma

One can show that a set P of paths are all $R$-paths with the co-induction principle:

okpath_co_ind
  ⊢ ∀P. (∀x r p. P (pcons x r p) ⇒ R x r (first p) ∧ P p) ⇒
         ∀p. P p ⇒ okpath R p

Character strings (string)

The theory string defines a type of characters and a type of finite strings built from those characters, along with a useful suite of definitions for operating on strings.

Characters. The type char is represented by the numbers less than 256. Two constants are defined: CHR : $\konst{num}\to\konst{char}$ and ORD : $\konst{char}\to\konst{num}$. The following theorems hold:

  CHR_ORD  |- !a. CHR (ORD a) = a
  ORD_CHR  |- !r. r < 256 = (ORD (CHR r) = r)

Character literals can also be entered using ML syntax, with a hash character immediately followed by a string literal of length one. Thus:

> load "stringTheory";
val it = (): unit
> val t = ``f #"c" #"\n"``;
<<HOL message: inventing new type variable names: 'a>>
val t = “f #"c" #"\n"”: term

> dest_comb ``#"\t"``;
val it = (“CHR”, “9”): term * term

Strings. The type string is an alias for the type char list. All functions and predicates over lists are thus available for use over strings. Some of these constants are overloaded so that they are printed (and can be parsed) with names that are more appropriate for the particular case of lists of characters.

For example, NIL and CONS over strings have alternative names EMPTYSTRING and STRING respectively:

   EMPTYSTRING : string
   STRING      : char -> string -> string

The HOL parser maps the syntax "" to EMPTYSTRING, and the HOL printer inverts this. The parser expands string literals of the form "$c_1\,c_2\,\ldots\,c_n$" to the compound term

$$ \mathtt{STRING}\;c_1\;(\mathtt{STRING}\;c_2\,\ldots\, (\mathtt{STRING}\;c_{n-1}\;(\mathtt{STRING}\; c_n\;\mathtt{EMPTYSTRING}))\,\ldots\,) $$

Of course, one could also write

> ``[#"a"; #"b"]``;
val it = “"ab"”: term

String literals can be constructed using the various special escape sequences that are used in ML. For example, \n for the newline character, and a backslash followed by three decimal digits for characters of the given number.

> val t = ``"foo bar\n\001"``;
val t = “"foo bar\n\^A"”: term

Note that if one wants to use the control-character syntax with the caret that the pretty-printer has chosen to use in printing the given string, and this occurs inside a quotation, then the caret will need to be doubled. (See Section 8.1.3.)

As with numerals, string literals can be injected into other types, where it might make sense to have string literals appear to inhabit types in addition to the core system's string type. Such literals can be written with different delimiters to make it clear that such an injection has occurred. For more on this facility, see the REFERENCE manual's description of the add_strliteral_form function.

There is also a destructor function DEST_STRING for strings which returns an option type:

DEST_STRING_def
  ⊢ DEST_STRING "" = NONE ∧
     ∀c rst. DEST_STRING (STRING c rst) = SOME (c,rst)

Case expressions. Compound HOL expressions that branch based on whether a term is an empty or non-empty string can be written with the surface syntax

   case s
    of "" => e1
     | STRING c rst => e2

Such an expression is actually a case-expression over the underlying list, and so the underlying constant is that for lists.

Length and concatenation. A standard function LENGTH can be written STRLEN when applied to a string, and APPEND can be written as STRCAT. There are also theorems characterising these constants in stringTheory, though they are simply instantiations of results from listTheory:

   STRLEN_THM
     |- (STRLEN "" = 0) /\
        (STRLEN (STRING c s) = 1 + STRLEN s)

   STRCAT_EQNS =
     |- (STRCAT "" s = s) /\
        (STRCAT s "" = s) /\
        (STRCAT (STRING c s1) s2 = STRING c (STRCAT s1 s2))

Strings into numbers, and vice versa.

It is natural to want to convert strings to and from (natural) numbers. Constants supporting this for a variety of bases ($2$, $8$, $10$, and $16$) are defined in the theory ASCIInumbersTheory. There the constants are named according to the scheme

$$ \mathtt{num\_}\{\mathtt{to},\mathtt{from}\}\mathtt{\_}\{\mathtt{bin},\mathtt{oct},\mathtt{dec},\mathtt{hex}\}\mathtt{\_string} $$

making for a total of eight constants. The two decimal constants are also available under the (overloaded) names toString and toNum. The natural theorem expressing these last two are inverse is

toNum_toString
  ⊢ ∀n. toNum (toString n) = n

and there is also a theorem specifying how long the strings produced by toString will be:

LENGTH_num_to_dec_string
  ⊢ STRLEN (toString n) = if n = 0 then 1 else LOG 10 n + 1

Collections

Several different notions of a collection of elements are available in HOL: sets, multisets, relations, and finite maps.

Sets (pred_set)

An extensive development of set theory is available in the theory pred_set. Sets are represented by functions of the type $\alpha\to\konst{bool}$, i.e., they are so-called characteristic functions. One can use the type abbreviation $\alpha\;\konst{set}$ instead of $\alpha\to\konst{bool}$. Sets may be finite or infinite. All of the elements in a set must have the same type.

Set membership is the basic notion that formalized set theory is based on. In HOL, membership is represented by a the infix constant IN, defined in theory bool for convenience.

IN_DEF
  ⊢ $IN = (λx f. f x)

The IN operator is merely a way of applying the characteristic function to an item, as the following trivial consequence of the definition shows:

SPECIFICATION
  ⊢ ∀P x. x ∈ P ⇔ P x

Two sets are equal if they have the same elements.

EXTENSION
  ⊢ ∀s t. s = t ⇔ ∀x. x ∈ s ⇔ x ∈ t

The negation of set-membership is not a separate constant, but is available as a convenient overload (in both ASCII and Unicode forms). Thus, instead of writing ~(e IN s), one can instead write e NOTIN s or e ∉ s.

Empty and universal sets. The empty set is the characteristic function that is constantly false. The constant EMPTY denotes the empty set; it may be written as {} and (U+2205). The universal set, UNIV, on a type is the characteristic function that is always true for elements of that type.

EMPTY_DEF
  ⊢ ∅ = (λx. F)
UNIV_DEF
  ⊢ 𝕌(:α) = (λx. T)

In addition to UNIV (perhaps with a type annotation :'a set), one may also write univ(:'a) to represent the universal set over type :'a. The Unicode syntax 𝕌(:'a) means the same. The Unicode symbol for $\mathbb{U}$ is U+1D54C, and may not exist in many fonts.

One of these forms will be used to print UNIV by default. The user trace (see Section 10.2) "Univ pretty-printing" can be set to zero to cancel this behaviour. Additionally, the trace "Unicode Univ printing" can be used to stop the U+1D54C syntax from being used, even if the Unicode trace is set.

The symbols univ and 𝕌 are high-priority prefixes (see Section 8.1.2.6), and overloaded patterns (see Section 8.1.2.3) mapping a value of the itself type to the corresponding UNIV constant. One effect is that one can write things like

   FINITE univ(:'a)

without the need for parentheses around FINITE's argument.

Insertion, union, and intersection. The insertion (INSERT, written infix) of an element into a set is defined with a set comprehension. Set comprehension is discussed in the next subsection. Set union (UNION, written infix) and intersection (INTER, also infix) are given their usual definitions by set comprehension.

INSERT_DEF
  ⊢ ∀x s. x INSERT s = {y | y = x ∨ y ∈ s}
UNION_DEF
  ⊢ ∀s t. s ∪ t = {x | x ∈ s ∨ x ∈ t}
INTER_DEF
  ⊢ ∀s t. s ∩ t = {x | x ∈ s ∧ x ∈ t}

UNION and INTER are binary operations. Indexed union and intersection operations, i.e., $\bigcup_{i\in P}$ and $\bigcap_{i\in P}$ are provided by the definitions of BIGUNION and BIGINTER.

BIGUNION
  ⊢ ∀P. BIGUNION P = {x | ∃s. s ∈ P ∧ x ∈ s}
BIGINTER
  ⊢ ∀P. BIGINTER P = {x | ∀s. s ∈ P ⇒ x ∈ s}

Both BIGUNION and BIGINTER reduce a set of sets to a set and thus have the type $((\alpha\to\konst{bool})\to\konst{bool})\to(\alpha\to\konst{bool})$.

Subsets. Set inclusion (SUBSET, infix), proper set inclusion (PSUBSET, infix), and power set (POW) are defined as follows:

SUBSET_DEF
  ⊢ ∀s t. s ⊆ t ⇔ ∀x. x ∈ s ⇒ x ∈ t
PSUBSET_DEF
  ⊢ ∀s t. s ⊂ t ⇔ s ⊆ t ∧ s ≠ t
POW_DEF
  ⊢ ∀set. POW set = {s | s ⊆ set}

Set difference and complement. The difference between two sets (DIFF, infix) is defined by a set comprehension. Based on that, the deletion of a single element (DELETE, infix) from a set is straightforward. Since the universe of a type is always available via UNIV, the complement (COMPL) of a set may be taken.

DIFF_DEF
  ⊢ ∀s t. s DIFF t = {x | x ∈ s ∧ x ∉ t}
DELETE_DEF
  ⊢ ∀s x. s DELETE x = s DIFF {x}
COMPL_DEF
  ⊢ ∀P. COMPL P = 𝕌(:α) DIFF P

Functions on sets. The image of a function $f:\alpha\to\beta$ on a set (IMAGE) is defined with a set comprehension.

IMAGE_DEF
  ⊢ ∀f s. IMAGE f s = {f x | x ∈ s}

Injections, surjections, and bijections between sets are defined as follows:

INJ_DEF
  ⊢ ∀f s t.
       INJ f s t ⇔
       (∀x. x ∈ s ⇒ f x ∈ t) ∧ ∀x y. x ∈ s ∧ y ∈ s ⇒ f x = f y ⇒ x = y
SURJ_DEF
  ⊢ ∀f s t.
       SURJ f s t ⇔
       (∀x. x ∈ s ⇒ f x ∈ t) ∧ ∀x. x ∈ t ⇒ ∃y. y ∈ s ∧ f y = x
BIJ_DEF
  ⊢ ∀f s t. BIJ f s t ⇔ INJ f s t ∧ SURJ f s t

Finite sets. The finite sets (FINITE) are defined inductively as those built from the empty set by a finite number of insertions.

FINITE_DEF
  ⊢ ∀s. FINITE s ⇔ ∀P. P ∅ ∧ (∀s. P s ⇒ ∀e. P (e INSERT s)) ⇒ P s

A set is infinite iff it is not finite, and there is an abbreviation in the system that parses "INFINITE s" into "~FINITE s". The pretty-printer reverses this transformation.

The finite sets have an induction theorem:

FINITE_INDUCT
  ⊢ ∀P. P ∅ ∧ (∀s. FINITE s ∧ P s ⇒ ∀e. e ∉ s ⇒ P (e INSERT s)) ⇒
         ∀s. FINITE s ⇒ P s

As mentioned, set operations apply to both finite and infinite sets. However, some operations, such as cardinality (CARD), are only defined for finite sets. (See Section 6.1.2 for the theory of cardinality of possibly infinite sets.)

CARD_DEF
  ⊢ CARD ∅ = 0 ∧
     ∀s. FINITE s ⇒
         ∀x. CARD (x INSERT s) = if x ∈ s then CARD s else SUC (CARD s)

Since the finite and infinite sets are dealt with uniformly in pred_set, properties of operations on finite sets must explicitly include constraints about finiteness. For example the following theorem relating cardinality and subsets is only true for finite sets.

CARD_PSUBSET
  ⊢ ∀s. FINITE s ⇒ ∀t. t ⊂ s ⇒ CARD t < CARD s

An extensive suite of theorems dealing with finiteness and cardinality is available in pred_set.

Cross product. The product of two sets (CROSS, infix) is defined with a set comprehension.

CROSS_DEF
  ⊢ ∀P Q. P × Q = {p | FST p ∈ P ∧ SND p ∈ Q}

Cardinality and cross product are related by the following theorem:

CARD_CROSS
  ⊢ ∀P Q. FINITE P ∧ FINITE Q ⇒ CARD (P × Q) = CARD P * CARD Q

Recursive functions on sets. Recursive functions on sets may be defined by wellfounded recursion. Usually, the totality of such a function is established by measuring the cardinality of the (finite) set. However, another theorem may be used to justify a fold (ITSET) for finite sets. Provided a function $f:\alpha\to\beta\to\beta$ obeys a condition known as left-commutativity, namely, $f\;x\;(f\;y\;z) = f\;y\;(f\;x\;z)$, then $f$ can be applied by folding it on the set in a tail-recursive fashion.

ITSET_THM
  ⊢ ∀s f b.
       FINITE s ⇒
       ITSET f s b = if s = ∅ then b else ITSET f (REST s) (f (CHOICE s) b)

ITSET_EMPTY
  ⊢ ∀f b. ITSET f ∅ b = b

COMMUTING_ITSET_INSERT
  ⊢ ∀f s.
       (∀x y z. f x (f y z) = f y (f x z)) ∧ FINITE s ⇒
       ∀x b. ITSET f (x INSERT s) b = ITSET f (s DELETE x) (f x b)

A recursive version is also available:

COMMUTING_ITSET_RECURSES
  ⊢ ∀f e s b.
       (∀x y z. f x (f y z) = f y (f x z)) ∧ FINITE s ⇒
       ITSET f (e INSERT s) b = f e (ITSET f (s DELETE e) b)

For the full derivation, see the sources of pred_set. The definition of ITSET allows, for example, the definition of summing the results of a function on a finite set of elements, from which a recursive characterization and other useful theorems are derived.

SUM_IMAGE_DEF
  ⊢ ∀f s. ∑ f s = ITSET (λe acc. f e + acc) s 0
SUM_IMAGE_THM
  ⊢ ∀f. ∑ f ∅ = 0 ∧
         ∀e s. FINITE s ⇒ ∑ f (e INSERT s) = f e + ∑ f (s DELETE e)

Other definitions and theorems. There are more definitions in pred_set, but they are not as heavily used as the ones presented here. Similarly, most theorems in pred_set relate the various common set operations to each other, but do not express any deep theorems of set theory.

However, one notable theorem is Koenig's Lemma, which states that every finitely branching infinite tree has an infinite path. There are many ways to formulate this theorem, depending on how the notion of tree is formalized. In HOL's formulation, the tree is characterised by making various assumptions about the finite-ness or otherwise of elements reachable using a relation R. Then the following version of Koenig's Lemma is stated and proved:

KoenigsLemma
  ⊢ ∀R. (∀x. FINITE {y | R x y}) ⇒
         ∀x. INFINITE {y | R꙳ x y} ⇒ ∃f. f 0 = x ∧ ∀n. R (f n) (f (SUC n))

Syntax for sets

The special purpose set-theoretic notations {$t_1; t_2; \ldots; t_n$} and {$t$|$p$} are recognized by the HOL parser and printer when the theory pred_set is loaded.

The normal interpretation of {$t_1;t_2;\ldots;t_n$} is the finite set containing just $t_1, t_2, \ldots, t_n$. This can be modelled by starting with the empty set and performing a sequence of insertions. For example, {1;2;3;4} parses to

   1 INSERT (2 INSERT (3 INSERT (4 INSERT EMPTY)))

Set comprehensions. The normal interpretation of {$t$|$p$} is the set of all $t$s such that $p$. In HOL, such syntax parses to:

GSPEC(\($x_1$,…,$x_n$).($t$,$p$))

where $x_1, \ldots, x_n$ are those free variables that occur in both $t$ and $p$ if both have at least one free variable. If $t$ or $p$ has no free variables, then $x_1,\ldots,x_n$ are taken to be the free variables of the other term. If both terms have free variables, but there is no overlap, then an error results. The order in which the variables are listed in the variable structure of the paired abstraction is an unspecified function of the structure of $t$ (it is approximately left to right). For example,

   {p+q | p < q /\ q < r}

parses to:

   GSPEC(\(p,q). ((p+q), (p < q /\ q < r)))

where GSPEC is characterized by:

GSPECIFICATION
  ⊢ ∀f v. v ∈ GSPEC f ⇔ ∃x. (v,T) = f x

This somewhat cryptic specification can be understood by exercising an example. The syntax

   a IN {p+q | p < q /\ q < r}

is mapped by the HOL parser to

   a IN GSPEC(\(p,q). ((p+q), (p < q /\ q < r)))

which, by GSPECIFICATION, is equal to

   ?x. (a,T) = (\(p,q). ((p+q), (p < q /\ q < r))) x

The existentially quantified variable x has a pair type, so it can be replaced by a pair (p,q) and a paired-$\beta$-reduction can be performed, yielding

   ?(p,q). (a,T) = ((p+q), (p < q /\ q < r))

which is equal to the intended meaning of the original syntax:

   ?(p,q). (a = p+q) /\ (p < q /\ q < r)

Unambiguous set comprehensions. There is also an unambiguous set comprehension syntax, which allows the user to specify which variables are to be quantified over in the abstraction that is the argument of GSPEC. Terms of the form

   { t | vs | P }

generate sets containing values of the form given by t, where the variables mentioned in vs must satisfy the constraint P. For example, the set

   { x + y | x | x < y }

is the set of numbers from y up to but not including 2 * y. The set can be “read” computationally: draw out all those x that are less than y, and to each such x add y, thereby generating a set of numbers.

In the example above, the underlying GSPEC term will be

   GSPEC (\x. (x + y, x < y))

The vs component of the unambiguous notation must be a single “variable structure” that might appear underneath a possibly paired abstraction as in Section 5.2.3.1. In other words, this

   { x + y | (x,y) | x < y }

is fine, but this

   { x + y | x y | x < y }

will raise an error. (Additionally, the outermost parentheses around pairs in the vs position can be omitted.)

The unambiguous notation is printed by the pretty-printer whenever the set to be printed can not be expressed with the default notation, or if the trace variable with name pp_unambiguous_comprehensions is set to 1. (If the same trace is set to 2, then the unambiguous notation will never be used.)

Decision procedure for set-theoretic theorems

HOL provides some tools (in bossLib) to automate the proof of some routine pred_set theorems by a reduction to first-order logic, ported from HOL Light. They are based on metisLib (see Section 8.4.2). Below are the entry-points: (see the REFERENCE manual for more details.)

   SET_TAC     : thm list -> tactic
   ASM_SET_TAC : thm list -> tactic
   SET_RULE    : term -> thm

The difference between SET_TAC and ASM_SET_TAC is that the latter one also makes use of assumptions. With them, many simple set-theoretic results can be directly proved without finding needed lemmas in pred_setTheory. For instance, a simple lemma from util_probTheory:

Theorem DISJOINT_RESTRICT_L :
  !s t c. DISJOINT s t ==> DISJOINT (s INTER c) (t INTER c)
Proof SET_TAC []
QED

Multisets (bag)

Multisets, also known as bags, are similar to sets, except that they allow repeat occurrences of an element. Whereas sets are represented by functions of type $\alpha\to\konst{bool}$, which signal the presence, or absence, of an element, multisets are represented by functions of type $\alpha\to\konst{num}$, which give the multiplicity of each element in the multiset. Multisets may be finite or infinite.

The type abbreviations $\alpha\;\konst{multiset}$ and $\alpha\;\konst{bag}$ can be used instead of $\alpha\to\konst{num}$.

Empty multiset. The empty bag has no elements. Thus, the function implementing it returns $0$ for every input.

EMPTY_BAG
  ⊢ {||} = K 0

The special syntax {||} (the analogue of [] for lists, and {} for sets) is used for both printing and parsing, but the underlying constant is indeed called EMPTY_BAG, and this name can also be passed to the parser.

Membership. Much of the theory can be based on the notion of membership in a bag. There are two notions: does an element occur at least $n$ times in a bag (BAG_INN); and does an element occur in a bag at all (BAG_IN).

BAG_INN
  ⊢ ∀e n b. BAG_INN e n b ⇔ b e ≥ n
BAG_IN
  ⊢ ∀e b. e ⋲ b ⇔ BAG_INN e 1 b

Two bags are equal if all elements have the same tally.

BAG_EXTENSION
  ⊢ ∀b1 b2. b1 = b2 ⇔ ∀n e. BAG_INN e n b1 ⇔ BAG_INN e n b2

Sub-multiset. A sub-bag relationship (SUB_BAG) holds between $b_1$ and $b_2$ provided that every element in $b_1$ occurs at least as often in $b_2$. The notion of a proper sub-bag (PSUB_BAG) is easily defined.

SUB_BAG
  ⊢ ∀b1 b2. b1 ≤ b2 ⇔ ∀x n. BAG_INN x n b1 ⇒ BAG_INN x n b2
PSUB_BAG
  ⊢ ∀b1 b2. b1 < b2 ⇔ b1 ≤ b2 ∧ b1 ≠ b2

Insertion. Inserting an element into a bag (BAG_INSERT) updates the tally for that element and leaves the others unchanged.

BAG_INSERT
  ⊢ ∀e b. BAG_INSERT e b = (λx. if x = e then b e + 1 else b x)

Explicitly-given multisets are supported by the syntax {|$t_1; t_2; \ldots; t_n$|}, where there may, of course, be repetitions. This is modelled by starting with the empty multiset and performing a sequence of insertions. For example, {|1; 2; 3; 2; 1|} parses to

   BAG_INSERT 1 (BAG_INSERT 2 (BAG_INSERT 3
                                 (BAG_INSERT 2 (BAG_INSERT 1 {||}))))

Union and difference. The union (BAG_UNION) and difference (BAG_DIFF) operations on bags both reduce to an arithmetic calculation on their elements. Deleting a single element from a bag may be expressed by taking the multiset difference with a single-element multiset; however, there is also a relational presentation (BAG_DELETE) which relates its first and last arguments only if the first contains exactly one more occurrence of the middle argument than the last. This is not the same as using BAG_DIFF to remove a one-element bag because it insists that the element being removed actually appear in the larger bag.

BAG_UNION
  ⊢ ∀b c. b ⊎ c = (λx. b x + c x)
BAG_DIFF
  ⊢ ∀b1 b2. b1 − b2 = (λx. b1 x − b2 x)
BAG_DELETE
  ⊢ ∀b0 e b. BAG_DELETE b0 e b ⇔ b0 = BAG_INSERT e b

Intersection, merge, and filter. The intersection of two bags (BAG_INTER) takes the pointwise minimum. The dual operation, merging (BAG_MERGE), takes the pointwise maximum. A bag can be ‘filtered’ by a set to return the bag where all the elements not in the set have been dropped (BAG_FILTER).

BAG_INTER
  ⊢ ∀b1 b2. BAG_INTER b1 b2 = (λx. if b1 x < b2 x then b1 x else b2 x)
BAG_MERGE
  ⊢ ∀b1 b2. BAG_MERGE b1 b2 = (λx. if b1 x < b2 x then b2 x else b1 x)
BAG_FILTER_DEF
  ⊢ ∀P b. BAG_FILTER P b = (λe. if P e then b e else 0)

Sets and multisets. Moving between bags and sets is accomplished by the following two definitions.

SET_OF_BAG
  ⊢ ∀b. SET_OF_BAG b = (λx. x ⋲ b)
BAG_OF_SET
  ⊢ ∀P. BAG_OF_SET P = (λx. if x ∈ P then 1 else 0)

Image. Taking the image of a function on a multiset to get a new multiset seems to be simply a matter of applying the function to each element of the multiset. However, there is a problem if $f$ is non-injective and the multiset is infinite. For example, take the multiset consisting of all the natural numbers and apply $\lambda x.\;1$ to each element. The resulting multiset would hold an infinite number of $1$s. To avoid this requires some constraints: for example, stipulating that the function be only finitely non-injective, or that the input multiset be finite. Such conditions would be onerous in proof; the compromise is to map the multipicity of problematic elements to $0$.

BAG_IMAGE_DEF
  ⊢ ∀f b.
       BAG_IMAGE f b =
       (λe.
            (let
               sb = BAG_FILTER (λe0. f e0 = e) b
             in
               if FINITE_BAG sb then BAG_CARD sb else 1))

Finite multisets. The finite multisets (FINITE_BAG) are defined inductively as those built from the empty bag by a finite number of insertions.

FINITE_BAG
  ⊢ ∀b. FINITE_BAG b ⇔ ∀P. P {||} ∧ (∀b. P b ⇒ ∀e. P (BAG_INSERT e b)) ⇒ P b

The finite multisets have an induction theorem, and also a strong induction theorem.

FINITE_BAG_INDUCT
  ⊢ ∀P. P {||} ∧ (∀b. P b ⇒ ∀e. P (BAG_INSERT e b)) ⇒ ∀b. FINITE_BAG b ⇒ P b

STRONG_FINITE_BAG_INDUCT
  ⊢ ∀P. P {||} ∧ (∀b. FINITE_BAG b ∧ P b ⇒ ∀e. P (BAG_INSERT e b)) ⇒
         ∀b. FINITE_BAG b ⇒ P b

The cardinality (BAG_CARD) of a multiset counts the total number of occurrences. It is only specified for finite multisets.

BAG_CARD_THM
  ⊢ BAG_CARD {||} = 0 ∧
     ∀b. FINITE_BAG b ⇒ ∀e. BAG_CARD (BAG_INSERT e b) = BAG_CARD b + 1

Recursive functions on multisets. Recursive functions on multiset may be defined by wellfounded recursion. Usually, the totality of such a function is established by measuring the cardinality of the (finite) multiset. However, a fold (ITBAG) for finite sets is provided. Provided a function $f:\alpha\to\beta\to\beta$ obeys a condition known as left-commutativity, namely, $f\;x\;(f\;y\;z) = f\;y\;(f\;x\;z)$, then $f$ can be applied by folding it on the multiset in a tail-recursive fashion.

ITBAG_EMPTY
  ⊢ ∀f acc. ITBAG f {||} acc = acc
COMMUTING_ITBAG_INSERT
  ⊢ ∀f b.
       (∀x y z. f x (f y z) = f y (f x z)) ∧ FINITE_BAG b ⇒
       ∀x a. ITBAG f (BAG_INSERT x b) a = ITBAG f b (f x a)

A recursive version is also available:

COMMUTING_ITBAG_RECURSES
  ⊢ ∀f e b a.
       (∀x y z. f x (f y z) = f y (f x z)) ∧ FINITE_BAG b ⇒
       ITBAG f (BAG_INSERT e b) a = f e (ITBAG f b a)

Relations (relation)

Mathematical relations can be represented in HOL by the type $\alpha\to\beta\to\konst{bool}$. (In most applications, the type of a relation is an instance of $\alpha\to\alpha\to\konst{bool}$, but the extra generality doesn't hurt.) The theory relation provides definitions of basic properties and operations on relations, defines various kinds of orders and closures, defines wellfoundedness and proves the wellfounded recursion theorem, and develops some basic results used in Term Rewriting.

Basic properties. The following basic properties of relations are defined.

transitive_def
  ⊢ ∀R. transitive R ⇔ ∀x y z. R x y ∧ R y z ⇒ R x z
reflexive_def
  ⊢ ∀R. reflexive R ⇔ ∀x. R x x
irreflexive_def
  ⊢ ∀R. irreflexive R ⇔ ∀x. ¬R x x
symmetric_def
  ⊢ ∀R. symmetric R ⇔ ∀x y. R x y ⇔ R y x
antisymmetric_def
  ⊢ ∀R. antisymmetric R ⇔ ∀x y. R x y ∧ R y x ⇒ x = y
equivalence_def
  ⊢ ∀R. equivalence R ⇔ reflexive R ∧ symmetric R ∧ transitive R
trichotomous
  ⊢ ∀R. trichotomous R ⇔ ∀a b. R a b ∨ R b a ∨ a = b
total_def
  ⊢ ∀R. total R ⇔ ∀x y. R x y ∨ R y x

Basic operations. The following basic operations on relations are defined: the empty relation (EMPTY_REL, or $\emptyset\subr$), relation composition (O, or $\circ\subr$, infix), inversion (inv, or $\_^{\mathsf{T}}$ (suffix superscript ‘T’)), domain (RDOM), and range (RRANGE).

EMPTY_REL_DEF
  ⊢ ∀x y. ∅ᵣ x y ⇔ F
O_DEF
  ⊢ ∀R1 R2 x z. (R1 ∘ᵣ R2) x z ⇔ ∃y. R2 x y ∧ R1 y z
inv_DEF
  ⊢ ∀R x y. Rᵀ x y ⇔ R y x
RDOM_DEF
  ⊢ ∀R x. RDOM R x ⇔ ∃y. R x y
RRANGE
  ⊢ ∀R y. RRANGE R y ⇔ ∃x. R x y

Set operations lifted to work on relations include subset (RSUBSET, or $\subseteq\subr$, infix), union (RUNION, or $\cup\subr$, infix), intersection (RINTER, or $\cap\subr$, infix), complement (RCOMPL), and universe (RUNIV, or $\mathbb{U}\subr$).

RSUBSET
  ⊢ ∀R1 R2. R1 ⊆ᵣ R2 ⇔ ∀x y. R1 x y ⇒ R2 x y
RUNION
  ⊢ ∀R1 R2 x y. (R1 ∪ᵣ R2) x y ⇔ R1 x y ∨ R2 x y
RINTER
  ⊢ ∀R1 R2 x y. (R1 ∩ᵣ R2) x y ⇔ R1 x y ∧ R2 x y
RCOMPL
  ⊢ ∀R x y. RCOMPL R x y ⇔ ¬R x y
RUNIV
  ⊢ ∀x y. 𝕌ᵣ x y ⇔ T

Orders. A sequence of definitions capturing various notions of order are made in relation.

PreOrder
  ⊢ ∀R. PreOrder R ⇔ reflexive R ∧ transitive R
Order
  ⊢ ∀Z. Order Z ⇔ antisymmetric Z ∧ transitive Z
WeakOrder
  ⊢ ∀Z. WeakOrder Z ⇔ reflexive Z ∧ antisymmetric Z ∧ transitive Z
StrongOrder
  ⊢ ∀Z. StrongOrder Z ⇔ irreflexive Z ∧ transitive Z
LinearOrder
  ⊢ ∀R. LinearOrder R ⇔ Order R ∧ trichotomous R
WeakLinearOrder
  ⊢ ∀R. WeakLinearOrder R ⇔ WeakOrder R ∧ trichotomous R
StrongLinearOrder
  ⊢ ∀R. StrongLinearOrder R ⇔ StrongOrder R ∧ trichotomous R

Closures. The transitive closure (TC) of a relation $R:\alpha\to\alpha\to\konst{bool}$ is defined inductively, as the least relation including $R$ and closed under transitivity. Similarly, the reflexive-transitive closure (RTC) is defined to be the least relation closed under transitivity and reflexivity. The ASCII syntax for the transitive closure R^+ is meant to suggest the prettier R⁺. Similarly, R^* is meant to suggest R∗. Indeed, with Unicode enabled, transitive closure will print with a superscript +, and RTC will print as a superscript asterisk.

From the underlying definitions, one can recover the initial rules:

TC_RULES
  ⊢ ∀R. (∀x y. R x y ⇒ R⁺ x y) ∧ ∀x y z. R⁺ x y ∧ R⁺ y z ⇒ R⁺ x z
RTC_RULES
  ⊢ ∀R. (∀x. R꙳ x x) ∧ ∀x y z. R x y ∧ R꙳ y z ⇒ R꙳ x z
RTC_RULES_RIGHT1
  ⊢ ∀R. (∀x. R꙳ x x) ∧ ∀x y z. R꙳ x y ∧ R y z ⇒ R꙳ x z

Notice that RTC_RULES, in keeping with the definition of RTC, extends an R-step from x to y with a sequence of R-steps from y to z to construct R* x z. The theorem RTC_RULES_RIGHT1 first makes a sequence of R steps and then a single R step to form R* x z. Similar alternative theorems are proved for case analysis and induction.

For example, TC_CASES1 and TC_CASES2 in the following decompose R+ x z to either R x y followed by R+ y z (TC_CASES1) or R+ x y followed by R y z (TC_CASES2).

TC_CASES1
  ⊢ R⁺ x z ⇔ R x z ∨ ∃y. R x y ∧ R⁺ y z
TC_CASES2
  ⊢ R⁺ x z ⇔ R x z ∨ ∃y. R⁺ x y ∧ R y z

RTC_CASES1
  ⊢ ∀R x y. R꙳ x y ⇔ x = y ∨ ∃u. R x u ∧ R꙳ u y
RTC_CASES2
  ⊢ ∀R x y. R꙳ x y ⇔ x = y ∨ ∃u. R꙳ x u ∧ R u y
RTC_CASES_RTC_TWICE
  ⊢ ∀R x y. R꙳ x y ⇔ ∃u. R꙳ x u ∧ R꙳ u y

As well as the basic induction theorems for TC and RTC, there are so-called strong induction theorems, which have stronger induction hypotheses.

TC_INDUCT
  ⊢ ∀R P.
       (∀x y. R x y ⇒ P x y) ∧ (∀x y z. P x y ∧ P y z ⇒ P x z) ⇒
       ∀u v. R⁺ u v ⇒ P u v
RTC_INDUCT
  ⊢ ∀R P.
       (∀x. P x x) ∧ (∀x y z. R x y ∧ P y z ⇒ P x z) ⇒
       ∀x y. R꙳ x y ⇒ P x y
TC_STRONG_INDUCT
  ⊢ ∀R P.
       (∀x y. R x y ⇒ P x y) ∧
       (∀x y z. P x y ∧ P y z ∧ R⁺ x y ∧ R⁺ y z ⇒ P x z) ⇒
       ∀u v. R⁺ u v ⇒ P u v
RTC_STRONG_INDUCT
  ⊢ ∀R P.
       (∀x. P x x) ∧ (∀x y z. R x y ∧ R꙳ y z ∧ P y z ⇒ P x z) ⇒
       ∀x y. R꙳ x y ⇒ P x y

Variants of these induction theorems are also available which break apart the closure from the left or right, as for the case analysis theorems.

The reflexive (RC) and symmetric closures (SC) are straightforward to define. The equivalence closure (EQC) is the symmetric then transitive then reflexive closure of $R$. When applied to an argument, as in EQC R, EQC is written with the suffix ^=. Note how the suffix binds more tightly than function application, so that in EQC_DEF, RC really is applied to the transitive closure of the symmetric closure of R.

RC_DEF
  ⊢ ∀R x y. RC R x y ⇔ x = y ∨ R x y
SC_DEF
  ⊢ ∀R x y. SC R x y ⇔ R x y ∨ R y x
EQC_DEF
  ⊢ ∀R. R^= = RC (SC R)⁺

Wellfounded relations.

A relation $R$ is wellfounded (WF) if every non-empty set has an $R$-minimal element. Wellfoundedness is used to justify the principle of wellfounded induction (WF_INDUCTION_THM).

WF_DEF
  ⊢ ∀R. WF R ⇔ ∀B. (∃w. B w) ⇒ ∃min. B min ∧ ∀b. R b min ⇒ ¬B b
WF_INDUCTION_THM
  ⊢ ∀R. WF R ⇒ ∀P. (∀x. (∀y. R y x ⇒ P y) ⇒ P x) ⇒ ∀x. P x

The wellfounded part (WFP) of a relation can be inductively defined, from which its rules, case-analysis theorem and induction theorems may be derived.

WFP_DEF
  ⊢ ∀R a. WFP R a ⇔ ∀P. (∀x. (∀y. R y x ⇒ P y) ⇒ P x) ⇒ P a
WFP_RULES
  ⊢ ∀R x. (∀y. R y x ⇒ WFP R y) ⇒ WFP R x
WFP_CASES
  ⊢ ∀R x. WFP R x ⇔ ∀y. R y x ⇒ WFP R y
WFP_INDUCT
  ⊢ ∀R P. (∀x. (∀y. R y x ⇒ P y) ⇒ P x) ⇒ ∀x. WFP R x ⇒ P x
WFP_STRONG_INDUCT
  ⊢ ∀R. (∀x. WFP R x ∧ (∀y. R y x ⇒ P y) ⇒ P x) ⇒ ∀x. WFP R x ⇒ P x

Wellfoundedness can also be used to justify a general recursion theorem. Intuitively, a collection of recursion equations can be admitted into the HOL logic with no loss of consistency provided that every possible sequence of recursive calls is finite. Wellfounded relations are used to capture this notion: if there is a wellfounded relation $R$ on the domain of the desired function such that every sequence of recursive calls is $R$-decreasing, then the recursion equations specify a unique total function and the equations can be admitted into the logic.

The recursion theorems WFREC_COROLLARY and WF_RECURSION_THM use the notion of a function restriction (RESTRICT) in order to force the recursive function to be applied to $R$-smaller arguments in recursive calls.

RESTRICT_DEF
  ⊢ ∀f R x. RESTRICT f R x = (λy. if R y x then f y else ARB)
WFREC_COROLLARY
  ⊢ ∀M R f. f = WFREC R M ⇒ WF R ⇒ ∀x. f x = M (RESTRICT f R x) x

WF_RECURSION_THM
  ⊢ ∀R. WF R ⇒ ∀M. ∃!f. ∀x. f x = M (RESTRICT f R x) x

The theorems WF_INDUCTION_THM and WFREC_COROLLARY are used to automate recursive definitions; see Section 7.6. A few basic operators for wellfounded relations are also defined, along with theorems stating that they propagate wellfoundedness.

inv_image_def
  ⊢ ∀R f. inv_image R f = (λx y. R (f x) (f y))

WF_inv_image
  ⊢ ∀R f. WF R ⇒ WF (inv_image R f)
WF_SUBSET
  ⊢ ∀R P. WF R ∧ (∀x y. P x y ⇒ R x y) ⇒ WF P
WF_TC
  ⊢ ∀R. WF R ⇒ WF R⁺
WF_EMPTY_REL
  ⊢ WF ∅ᵣ

Term Rewriting. A few basic definitions from Term Rewriting theory (the diamond property (diamond), the Church-Rosser property (CR and WCR), and Strong Normalization (SN)) appear in relation.

diamond_def
  ⊢ ∀R. diamond R ⇔ ∀x y z. R x y ∧ R x z ⇒ ∃u. R y u ∧ R z u
CR_def
  ⊢ ∀R. CR R ⇔ diamond R꙳
WCR_def
  ⊢ ∀R. WCR R ⇔ ∀x y z. R x y ∧ R x z ⇒ ∃u. R꙳ y u ∧ R꙳ z u
SN_def
  ⊢ ∀R. SN R ⇔ WF Rᵀ

From those, Newman's Lemma is proved.

Newmans_lemma
  ⊢ ∀R. WCR R ∧ SN R ⇒ CR R

Bisimulation for labelled transition systems (bisimulation)

HOL provides a minimal theory (bisimulation) for generating bisimulation and bisimulation relations (Milner 1989) from any labelled transition relation of the type $\alpha\to\beta\to\alpha\to\konst{bool}$. Suppose there is a user-defined labelled transition system (LTS) and ts is the transition relation in it, then ts p l q denotes a transition from $p$ to $q$ by an action $l$, i.e., $p\overset{l}{\longrightarrow}q$. A binary relation $R$ is called a bisimulation if BISIM ts R holds under the following definition:

BISIM_def
  ⊢ ∀ts R.
       BISIM ts R ⇔
       ∀p q.
         R p q ⇒
         ∀l. (∀p'. ts p l p' ⇒ ∃q'. ts q l q' ∧ R p' q') ∧
             ∀q'. ts q l q' ⇒ ∃p'. ts p l p' ∧ R p' q'

Furthermore, the bisimulation relation (or bisimilarity, usually denoted by $\sim$), BISIM_REL ts, is the union (in the sense of RUNION) of all bisimulations in this LTS, and it can be proven to be an equivalence relation: (the original definition hereafter)

BISIM_REL_def
  ⊢ ∀ts. BISIM_REL ts = (λp q. ∃R. BISIM ts R ∧ R p q)
BISIM_REL_IS_EQUIV_REL
  ⊢ ∀ts. equivalence (BISIM_REL ts)

Bisimulation is a special case of coinduction, by far the most studied coinductive concept (Sangiorgi 2012). In practice it is hard to directly work with the above definition of BISIM_REL. Partly for this reason, BISIM_REL is defined by the coinductive relation package (see Section 7.7.2):

CoInductive BISIM_REL :
    !p q. (!l.
            (!p'. ts p l p' ==> ?q'. ts q l q' /\ (BISIM_REL ts) p' q') /\
            (!q'. ts q l q' ==> ?p'. ts p l p' /\ (BISIM_REL ts) p' q'))
      ==> (BISIM_REL ts) p q
End

which automatically generates the following coinduction principle:

BISIM_REL_coind
  ⊢ ∀ts BISIM_REL'.
       (∀a0 a1.
          BISIM_REL' a0 a1 ⇒
          ∀l. (∀p'. ts a0 l p' ⇒ ∃q'. ts a1 l q' ∧ BISIM_REL' p' q') ∧
              ∀q'. ts a1 l q' ⇒ ∃p'. ts a0 l p' ∧ BISIM_REL' p' q') ⇒
       ∀a0 a1. BISIM_REL' a0 a1 ⇒ BISIM_REL ts a0 a1

A simple rewrite by BISIM_def shows that BISIM_REL defined in this way is actually equivalent to the original definition (which now becomes a theorem):14

> Q.SPECL [`ts`, `R`]
          (REWRITE_RULE [GSYM BISIM_def, GSYM RSUBSET] BISIM_REL_coind);
val it = ⊢ BISIM ts R ⇒ R ⊆ᵣ BISIM_REL ts: thm

The bisimulation and bisimulation relation considered so far are the strong ones, in the sense that all involved transitions are one-step transitions. To define weak bisimulation (and the corresponding weak bisimulation relation), beside the transition relation, the user must also designate a special unique action to be invisible (usually denoted by $\tau$). An empty transition (ETS) is the reflexive-transitive closure (RTC) of (one-step) invisible transitions:

ETS_def
  ⊢ ∀ts tau. ETS ts tau = (λx y. ts x tau y)꙳

Then a weak transition (WTS) is a one-step transition (the action may be invisible) concatenated with two empty transitions:

WTS_def
  ⊢ ∀ts tau.
       WTS ts tau =
       (λp l q. ∃p' q'. ETS ts tau p p' ∧ ts p' l q' ∧ ETS ts tau q' q)

An empty transition ETS ts tau p q is usually denoted by $p\overset{\varepsilon}{\Longrightarrow}q$ (assuming ts and tau is clear from the context), and a weak transition WTS ts tau p l q is denoted by $p\overset{l}{\Longrightarrow}q$. Note that a weak transition must have at least one transition (even it is $\tau$), while an empty transition may have no actual transition at all. To simplify the informal definitions, we denote by $p\overset{\hat{a}}{\Longrightarrow}q$ the special notion of weak transitions such that, whenever the action $a$ is invisible, it is the same as $p\overset{\varepsilon}{\Longrightarrow}q$.

A binary relation $R$ is a weak bisimulation if, whenever $p\;R\;q$,

  • $p\overset{a}{\longrightarrow}p'$ implies that there is $q'$ such that $q\overset{\hat{a}}{\Longrightarrow}q'$ and $p'\;R\;q'$;
  • $q\overset{a}{\longrightarrow}q'$ implies that there is $p'$ such that $p\overset{\hat{a}}{\Longrightarrow}p'$ and $p'\;R\;q'$.

$p$ and $p$ are weakly bisimilar, written as $p\approx q$, if $p\;R\;q$ for some bisimulation $R$.

Following the example of the strong case (including the use of the CoInductive command), below are the definition of weak bisimulation (WBISIM), the equivalent definition of weak bisimulation relation (WBISIM_REL) and the theorem saying WBISIM_REL ts tau is indeed an equivalence relation (in the LTS given by ts and tau):

WBISIM_def
  ⊢ ∀ts tau R.
       WBISIM ts tau R ⇔
       ∀p q.
         R p q ⇒
         (∀l. l ≠ tau ⇒
              (∀p'. ts p l p' ⇒ ∃q'. WTS ts tau q l q' ∧ R p' q') ∧
              ∀q'. ts q l q' ⇒ ∃p'. WTS ts tau p l p' ∧ R p' q') ∧
         (∀p'. ts p tau p' ⇒ ∃q'. ETS ts tau q q' ∧ R p' q') ∧
         ∀q'. ts q tau q' ⇒ ∃p'. ETS ts tau p p' ∧ R p' q'
WBISIM_REL_def
  ⊢ ∀ts tau. WBISIM_REL ts tau = (λp q. ∃R. WBISIM ts tau R ∧ R p q)
WBISIM_REL_IS_EQUIV_REL
  ⊢ ∀ts tau. equivalence (WBISIM_REL ts tau)

In examples/CCS, the HOL distribution includes a comprehensive formalization of Milner's Calculus of Communicating Systems (CCS) (Milner 1989), where the definitions of strong and weak bisimulations are based on the present bisimulation theory.

Finite maps (finite_map)

The theory finite_map formalizes a type $(\alpha,\beta)\,\mathtt{fmap}$ of finite functions. These notionally have type $\alpha\to\beta$, but additionally have only finitely many elements in their domain. Finite maps are useful for formalizing substitutions and arrays. The representing type is $\alpha\to\beta+\konst{one}$, where only a finite number of the $\alpha$ map to a $\beta$ and the rest map to one. The syntax $\alpha\,\mathtt{|->}\,\beta$ is recognized by the parser as an alternative to $(\alpha,\beta)\,\mathtt{fmap}$.

Basic notions. The empty map (FEMPTY), the updating of a map (FUPDATE), the application of a map to an argument (FAPPLY), and the domain of a map (FDOM) are the main notions in the theory.

   FEMPTY  : 'a |-> 'b
   FUPDATE : ('a |-> 'b) -> 'a # 'b -> ('a |-> 'b)
   FAPPLY  : ('a |-> 'b) -> 'a -> 'b
   FDOM    : ('a |-> 'b) -> 'a set

The HOL parser and printer will treat the syntax f ' x as the application of finite map f to argument x, i.e., as FAPPLY f x. The notation f |+ (k,v) represents FUPDATE f (k,v), i.e., the updating of finite map f by the pair (k,v). These are purely ASCII syntaxes, but the HOL printer and parser also support (and prefer, in the case of printing), $f\langle k\rangle$ for finite map application, and $f\langle k\mapsto v\rangle$ for FUPDATE. As with lists (see Section 5.4.1.2), the update syntax between angle brackets supports multiple updates, allowing $f\langle k_1\mapsto v_1;\;k_2\mapsto v_2;\dots\rangle$.

The basic constants have obscure definitions, from which more useful properties are then derived. FAPPLY_FUPDATE_THM relates map update with map application. fmap_EXT is an extensionality result: two maps are equal if they have the same domain and agree when applied to arguments in that domain. One can prove properties of finite maps by induction on the construction of the map (fmap_INDUCT). The cardinality of a finite map is just the cardinality of its domain (FCARD_DEF); from this a recursive characterization (FCARD_FUPDATE) is derived.

   FAPPLY_FUPDATE_THM
     ⊢ ∀f a b x. f⟨a ↦ b⟩⟨x⟩ = if x = a then b else f⟨x⟩
   fmap_EXT
     ⊢ ∀f g. f = g ⇔ FDOM f = FDOM g ∧ ∀x. x ∈ FDOM f ⇒ f⟨x⟩ = g⟨x⟩
   fmap_INDUCT
     ⊢ ∀P. P FEMPTY ∧ (∀f. P f ⇒ ∀x y. x ∉ FDOM f ⇒ P f⟨x ↦ y⟩) ⇒ ∀f. P f
   FCARD_DEF
     ⊢ ∀fm. FCARD fm = CARD (FDOM fm)
   FCARD_FUPDATE
     ⊢ ∀fm a b.
          FCARD fm⟨a ↦ b⟩ = if a ∈ FDOM fm then FCARD fm else 1 + FCARD fm

Iterated updates (FUPDATE_LIST) to a map are useful. The infix notation |++ may also be used. For example, fm |++ [(k1,v1); (k2,v2)] is equal to (fm |+ (k1,v1)) |+ (k2,v2).

   FUPDATE_LIST
     ⊢ $|++ = FOLDL $|+
   FUPDATE_LIST_THM
     ⊢ ∀f. f |++ [] = f ∧ ∀h t. f |++ (h::t) = f |+ h |++ t

Domain and range. The domain of a finite map is the set of elements that it applies to; this can be characterized recursively (FDOM_FUPDATE). The range of a map is defined in the usual way.

   FDOM_FUPDATE
     ⊢ ∀f a b. FDOM f⟨a ↦ b⟩ = a INSERT FDOM f
   FRANGE_DEF
     ⊢ ∀f. FRANGE f = {y | ∃x. x ∈ FDOM f ∧ f⟨x⟩ = y}

A finite map may have its domain (DRESTRICT) or range (RRESTRICT) restricted by intersection with a set. These notions have recursive versions as well (DRESTRICT_FUPDATE and RRESTRICT_FUPDATE).

   DRESTRICT_DEF
     ⊢ ∀f r.
          FDOM (DRESTRICT f r) = FDOM f ∩ r ∧
          ∀x. (DRESTRICT f r)⟨x⟩ = if x ∈ FDOM f ∩ r then f⟨x⟩ else FEMPTY⟨x⟩
   RRESTRICT_DEF
     ⊢ ∀f r.
          FDOM (RRESTRICT f r) = {x | x ∈ FDOM f ∧ f⟨x⟩ ∈ r} ∧
          ∀x. (RRESTRICT f r)⟨x⟩ =
              if x ∈ FDOM f ∧ f⟨x⟩ ∈ r then f⟨x⟩ else FEMPTY⟨x⟩
   DRESTRICT_FUPDATE
     ⊢ ∀f r x y.
          DRESTRICT f⟨x ↦ y⟩ r =
          if x ∈ r then (DRESTRICT f r)⟨x ↦ y⟩ else DRESTRICT f r
   RRESTRICT_FUPDATE
     ⊢ ∀f r x y.
          RRESTRICT f⟨x ↦ y⟩ r =
          if y ∈ r then (RRESTRICT f r)⟨x ↦ y⟩
          else RRESTRICT (DRESTRICT f (COMPL {x})) r

The removal of a single element from the domain of a map (\\, infix) is a simple application of DRESTRICT, but sufficiently useful to deserve its own definition. Again, this concept has a alternate recursive presentation (DOMSUB_FUPDATE_THM).

   fmap_domsub
     ⊢ ∀fm k. fm \\ k = DRESTRICT fm (COMPL {k})
   DOMSUB_FUPDATE_THM
     ⊢ ∀fm k1 k2 v.
          fm⟨k1 ↦ v⟩ \\ k2 = if k1 = k2 then fm \\ k2 else (fm \\ k2)⟨k1 ↦ v⟩

Similarly, the removal of multiple elements from the domain of a map (FDIFF) is defined in terms of DRESTRICT. It too has an alternate recursive presentation.

   FDIFF_def
     ⊢ ∀f1 s. FDIFF f1 s = DRESTRICT f1 (COMPL s)
   FDIFF_FUPDATE
     ⊢ FDIFF fm⟨k ↦ v⟩ s = if k ∈ s then FDIFF fm s else (FDIFF fm s)⟨k ↦ v⟩

Union and sub-maps. Unlike set union, the union of two finite maps (FUNION) is not symmetric: the domain of the first map takes precedence. The notion of a finite map being a submap of another (SUBMAP, infix) is an extension of how subsets are formalized.

   FUNION_DEF
     ⊢ ∀f g.
          FDOM (f ⊌ g) = FDOM f ∪ FDOM g ∧
          ∀x. (f ⊌ g)⟨x⟩ = if x ∈ FDOM f then f⟨x⟩ else g⟨x⟩
   SUBMAP_DEF
     ⊢ ∀f g. f ⊑ g ⇔ ∀x. x ∈ FDOM f ⇒ x ∈ FDOM g ∧ f⟨x⟩ = g⟨x⟩

Merges. The key-aware merge of two finite maps (FMERGE_WITH_KEY) generalises the left-biased union of two finite maps (FUNION). In FMERGE_WITH_KEY f m1 m2, rather than the domain of m1 taking precedence (as in FUNION), overlapping keys and their associated values are processed by the function parameter f.

   FMERGE_WITH_KEY_DEF
     ⊢ ∀f m1 m2.
          FDOM (FMERGE_WITH_KEY f m1 m2) = FDOM m1 ∪ FDOM m2 ∧
          ∀x. (FMERGE_WITH_KEY f m1 m2)⟨x⟩ =
              if x ∈ FDOM m1 ∧ x ∈ FDOM m2 then f x m1⟨x⟩ m2⟨x⟩
              else if x ∈ FDOM m1 then m1⟨x⟩
              else m2⟨x⟩

The key-ignorant merge of two finite maps (FMERGE) specialises FMERGE_WITH_KEY, and is itself a generalisation of FUNION.

   FMERGE_WITH_KEY_FMERGE
     ⊢ FMERGE f = FMERGE_WITH_KEY (λk v1 v2. f v1 v2)
   FMERGE_FUNION
     ⊢ FUNION = FMERGE (λx y. x)

Finite maps and functions. As much as possible, finite maps should be like ordinary functions. Thus, if f is a finite map, then FAPPLY f is an ordinary function. Similarly, there is an operation for totalizing a finite map (FLOOKUP) so that an application of it returns an ordinary function, the range of which is the option type. An ordinary function can be turned into a finite map by restricting the function to a finite set of arguments (FUN_FMAP_DEF).

   FLOOKUP_DEF
     ⊢ ∀f x. FLOOKUP f x = if x ∈ FDOM f then SOME f⟨x⟩ else NONE

   FUN_FMAP_DEF
     ⊢ ∀f P.
          FINITE P ⇒
          FDOM (FUN_FMAP f P) = P ∧ ∀x. x ∈ P ⇒ (FUN_FMAP f P)⟨x⟩ = f x

Composition of maps.

There are three new definitions of composition, determined by whether the composed functions are finite maps or not. The composition of two finite maps (f_o_f, infix) has domain constraints attached. Composition of a finite map with an ordinary function (o_f, infix) applies the finite map first, then the ordinary function. Composition of an ordinary function with a finite map (f_o, infix) applies the ordinary function and then the finite map; the application of the ordinary function is achieved by turning it into a finite map.

   f_o_f_DEF
     ⊢ ∀f g.
          FDOM (f f_o_f g) = FDOM g ∩ {x | g⟨x⟩ ∈ FDOM f} ∧
          ∀x. x ∈ FDOM (f f_o_f g) ⇒ (f f_o_f g)⟨x⟩ = f⟨g⟨x⟩⟩
   o_f_DEF
     ⊢ ∀f g.
          FDOM (f o_f g) = FDOM g ∧
          ∀x. x ∈ FDOM (f o_f g) ⇒ (f o_f g)⟨x⟩ = f g⟨x⟩
   f_o_DEF
     ⊢ ∀f g. f f_o g = f f_o_f FUN_FMAP g {x | g x ∈ FDOM f}

While Loops

It is a curious fact that higher order logic, although a logic of total functions, allows the definition of functions that don't seem total, at least from a computational perspective. An example is WHILE-loops. The following equation is derived in theory While:

WHILE
  ⊢ ∀P g x. WHILE P g x = if P x then WHILE P g (g x) else x

Clearly, if P in this theorem was instantiated to $\lambda x.\;\konst{T}$, the resulting instance of WHILE would ‘run forever’ if executed. Why is such an “obviously” partial function definable in HOL? The answer lies in a subtle definition of WHILE,15 which uses the expressive power of HOL to surprising effect. Consider the following total and non-recursive function:

  \x. if (?n. P (FUNPOW g n x))
       then FUNPOW g (@n. P (FUNPOW g n x) /\
                          !m.  m < n ==> ~P (FUNPOW g m x)) x
       else ARB

This function does a case analysis on the iterations of function g: the finite ones return the first value in the iteration at which P holds (i.e., when the iteration stops); the infinite ones are mapped to ARB. This function is used as the witness for f in the proof of the following theorem:

ITERATION
  ⊢ ∀P g. ∃f. ∀x. f x = if P x then x else f (g x)

From this, it is a simple application of Skolemization and new_specification to obtain the equation for WHILE.

Reasoning about WHILE loops. The induction theorem for WHILE loops is proved by wellfounded induction, and carries wellfoundedness constraints limiting its application. In order to apply WHILE_INDUCTION, the instantiations for B and C must be known before a wellfounded relation for R is found and used to eliminate the constraints.

WHILE_INDUCTION
  ⊢ ∀B C R.
       WF R ∧ (∀s. B s ⇒ R (C s) s) ⇒
       ∀P. (∀s. (B s ⇒ P (C s)) ⇒ P s) ⇒ ∀v. P v

A more refined level of support is provided by the standard Hoare Logic WHILE rule, phrased in terms of Hoare triples (HOARE_SPEC).

HOARE_SPEC_DEF
  ⊢ ∀P C Q. HOARE_SPEC P C Q ⇔ ∀s. P s ⇒ Q (C s)
WHILE_RULE
  ⊢ ∀R B C.
       WF R ∧ (∀s. B s ⇒ R (C s) s) ⇒
       HOARE_SPEC (λs. P s ∧ B s) C P ⇒
       HOARE_SPEC P (WHILE B C) (λs. P s ∧ ¬B s)

As a follow-on, an operator for finding the least number with property P is defined.

LEAST_DEF
  ⊢ ∀P. $LEAST P = WHILE ($¬ ∘ P) SUC 0

The LEAST constant is treated as a binder by the parser, which explains the special printing of the name above. Its use as a binder can be seen in:

LEAST_LESS_EQ
  ⊢ (LEAST x. y ≤ x) = y

A fundamental result, the operation of which is embodied in the tactic LEAST_ELIM_TAC is:

LEAST_ELIM
  ⊢ ∀Q P. (∃n. P n) ∧ (∀n. (∀m. m < n ⇒ ¬P m) ∧ P n ⇒ Q n) ⇒ Q ($LEAST P)

If one wants a specified result even when the predicate is everywhere false, the OLEAST function (also treated as a binder by the parser and pretty-printer) may be helpful:

OLEAST_def
  ⊢ ∀P. $OLEAST P = if ∃n. P n then SOME (LEAST n. P n) else NONE

More theorems for reasoning about LEAST, OLEAST and WHILE may be found in theory While.

Monads

HOL's simple type system means that it is impossible to define a general type of monad in the way that is possible in programming languages such as Haskell. Nonetheless, a number of the types predefined in HOL, such as options and lists, can indeed be seen as monads, and it is useful to be able to write functions over those types that leverage this view. Equally, it is useful to be able to declare monads of one's own that can use the same syntactic facilities.

Monads are defined by their “unit” and “bind” functions, and these can be composed in expressive ways. In particular, HOL supports a syntax inspired by Haskell's do notation, wherein it is possible to write such functions as

> Definition mapM_def:
    mapM f [] = return [] ∧
    mapM f (x::xs) = do
       e  <- f x;
       es <- mapM f xs;
       return (e::es);
     od
  End
<<HOL message: inventing new type variable names: 'a, 'b>>
Definition has been stored under "mapM_def"
val mapM_def =
   ⊢ (∀f. mapM f [] = [[]]) ∧
     ∀f x xs. mapM f (x::xs) = do e <- f x; es <- mapM f xs; [e::es] od: thm

> type_of “mapM”;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = “:(α -> β list) -> α list -> β list list”: hol_type

Again, because HOL does not have a sufficiently expressive type system, though the notation is generic, the function is fixed to a particular monad instance. In this case, the monad instance is that of lists. We can use the mapM function to implement what one might term a cross-product operation on lists:

> EVAL “mapM I [[1;2;3]; [a;b]; [x;y;z]]”;
val it =
   ⊢ mapM I [[1; 2; 3]; [a; b]; [x; y; z]] =
     [[1; a; x]; [1; a; y]; [1; a; z]; [1; b; x]; [1; b; y]; [1; b; z];
      [2; a; x]; [2; a; y]; [2; a; z]; [2; b; x]; [2; b; y]; [2; b; z];
      [3; a; x]; [3; a; y]; [3; a; z]; [3; b; x]; [3; b; y]; [3; b; z]]: thm

The general abstract syntax is described by the following grammar

$$ \begin{array}{rcl} M &::=& e_{\alpha\,\mathtt{M}} \;\;\mid\;\; \mathtt{do}\;\,\mathit{binds}\,\;\mathtt{od} \;\;\mid\;\; \mathtt{return}\;e_\alpha\\ \mathit{binds} &::=& \mathit{bind}\;\mathtt{;}^? \;\;\mid\;\; \mathit{bind}\,\mathtt{;}\,\;\mathit{binds}\\ \mathit{bind} &::=& M \;\;\mid\;\; \mathit{vs}\;\,\mathtt{<-}\;M \;\;\mid\;\; \mathit{vs}\;\,\mathtt{<<-}\;e_\alpha \end{array} $$

where $\mathit{e}_\tau$ is a HOL expression required to be of type $\tau$, and $\mathit{vs}$ is a single variable, or a tuple of variables (e.g., (x,y,z)). If a given $M$ has type :$\alpha$ M for a monad instance M, then writing a binding such as v <- M will require variable $v$ to have type $\alpha$. This variable is then bound in later bindings within the same do-od block. One can also bind variables directly to expressions of the appropriate type with the <<- arrow. This corresponds to an underlying let-term, but equally, one can see v <<- e as semantically equivalent to v <- return e.

The special monad syntax has a straightforward translation into underlying HOL terms. The do-od delimiters have no semantic effect; they can be viewed as a special form of parenthesis that identifies where the binding syntax is going to be used.16 Subsequently, the translation from $\mathit{vs}\;\mathtt{<-}\,M_1\mathtt{;}\;M_2$ is to $\mathtt{monad\_bind}\;M_1\;(\lambda\mathit{vs}.\,M_2)$, making it clear that $\mathit{vs}$ can be used (and is bound) in $M_2$. If there is no variable-arrow on the first binding, then $M_1\mathtt{;}\;M_2$ translates to something equivalent to $\mathtt{monad\_bind}\;M_1\;(\mathtt{K}\;M_2)$, where K is the K-combinator from combinTheory (see Section 5.2.2). As already suggested, $\mathit{vs}\;\mathtt{<<-}\;e\mathtt{;}\;M$ translates to $\mathtt{let}\;\mathit{vs}\;\mathtt{=}\;e\;\mathtt{in}\;M$. Finally, the return keyword is an overloading for the monad instance's unit function (which will have type $\alpha\to\alpha\,\mathtt{M}$).

Declaring monads

Monad instances have to be declared if the system is to support their parsing and pretty-printing. The function responsible is declare_monad in the monadsyntax module.

type monadinfo =
   \{bind: term,
     choice: term option,
     fail: term option,
     guard: term option, ignorebind: term option, unit: term\}
val declare_monad = fn: string * monadsyntax.monadinfo -> unit

The terms for the bind and unit fields are the terms implementing the corresponding monad functions. For example, the bind function for the option function is OPTION_BIND, characterised by its defining theorem:

OPTION_BIND_def
  ⊢ (∀f. OPTION_BIND NONE f = NONE) ∧ ∀x f. OPTION_BIND (SOME x) f = f x

The unit function is just the SOME constructor for the type.

All of the other fields in the monadinfo record can be left unspecified. The ignorebind field is used to encode the situation where the user writes

   do m1; m2 od

meaning that though m1 may return a value, the remainder of the function does not use that value. As above, this can be handled through the use of the K combinator, which is what is done if the ignorebind field is set to NONE. However, if desired, one can specify a specific term to be used in this situation. For example, the system's encoding of the option monad uses a separate constant with exactly the definition one would expect:

OPTION_IGNORE_BIND_def
  ⊢ ∀m1 m2. OPTION_IGNORE_BIND m1 m2 = OPTION_BIND m1 (K m2)

The three remaining fields (guard, fail and choice) are relevant for monads with errors or failure modes. The underlying property is that if $m_1$ is an error value, then $\mathtt{monad\_bind}\;m_1\;f \;\;=\;\; m_1$, meaning that attempting to sequence computations after an error just causes the error to be the result, without any use of the $f$ value. In this way, one might see $m_1$ as a computation that has thrown some sort of exception.

If one specifies a fail term in declaring a monad, an overload is set up from the string fail to that term. This is flexible enough to allow parameterised errors: make the term be the function that takes a parameter and returns a monad error value. There is no monad-specific support required for this concept: the overload is sufficient.

When provided, the guard and choice terms are similarly established as overloads so that monadic code across different monads will look similar. If one views fail as throwing an exception, then the choice notation allows for catching an exception and trying another computation. The overload is to the infix ++ syntax, meaning that one can write $M_1\;\mathtt{++}\;M_2$ to represent the “choice” between $M_1$ and $M_2$. (Note that in the list monad, ++ is APPEND. This explains the choice of notation, though in the list monad, viewing “choice” as throwing and catching exceptions is actually harder to motivate.)

Finally, the guard term (if given) overloads to the term assert, which is expected to be defined such that

$$ \mathtt{assert}\;b\;\; = \;\;\textsf{if}\;b\;\textsf{then}\;\mathtt{return}\;()\;\textsf{else}\;\mathtt{fail} $$

A typical use of assert might be in a function such as

  do
     list <- some_monad;
     assert(list <> []);
     return (HD list + 1)
  od

where the assert ensures that the subsequent call to HD makes sense.

Enabling monad syntax

There are two steps to being able to use monadic syntax. Both steps persist, meaning that their effects are preserved for the benefit of descendant theories. Because of this persistence, the function calls implementing these two steps should only be used in xScript.sml files. As with other parsing and pretty-printing functions, there are temp_ versions of the functions. These do not cause persistence and can safely be used in other .sml files (such as library implementations).

The first step is to enable the generic monad syntax, by calling

   monadsyntax.enable_monadsyntax : unit -> unit

After this, one can write dood blocks, even though without any specific instances enabled the output will be unhelpful (the monad_bind printed in the session below is actually a variable):

> monadsyntax.enable_monadsyntax();
val it = (): unit
> “do x <- M1; M2 od”;
<<HOL message: inventing new type variable names: 'a, 'b, 'c, 'd>>
val it = “monad_bind M1 (λx. M2)”: term

One can see which monads have been declared with a call to all_monads:

> monadsyntax.all_monads()
val it =
   [("list",
     {bind = “LIST_BIND”, choice = SOME (“APPEND”), fail = SOME (“[]”),
      guard = SOME (“LIST_GUARD”), ignorebind = SOME (“LIST_IGNORE_BIND”),
      unit = “λx. [x]”}),
    ("option",
     {bind = “OPTION_BIND”, choice = SOME (“OPTION_CHOICE”), fail =
      SOME (“NONE”), guard = SOME (“OPTION_GUARD”), ignorebind =
      SOME (“OPTION_IGNORE_BIND”), unit = “SOME”})]:
   (string * monadsyntax.monadinfo) list

Particular monads can be enabled with calls to enable_monad. The most recently enabled is preferred when the context makes the choice ambiguous.

> List.app monadsyntax.enable_monad ["list", "option"];
val it = (): unit

> val t = “do x <- M; return (x + 1) od”;
<<HOL message: more than one resolution of overloading was possible>>
val t = “do x <- M; SOME (x + 1) od”: term
> type_of t;
val it = “:num option”: hol_type

> val t' = “do x <- MAP f l; return (x + 1); od”;
<<HOL message: inventing new type variable names: 'a>>
val t' = “do x <- MAP f l; [x + 1] od”: term
> type_of t';
val it = “:num list”: hol_type

Thanks to the persistence features of these API points, loading fresh theories may cause more monads to be declared and/or enabled:

> load "errorStateMonadTheory";
val it = (): unit
> monadsyntax.all_monads();
val it =
   [("errorState",
     {bind = “BIND”, choice = SOME (“ES_CHOICE”), fail = SOME (“ES_FAIL”),
      guard = SOME (“ES_GUARD”), ignorebind = SOME (“IGNORE_BIND”), unit =
      “UNIT”}),
    ("list",
     {bind = “monad_bind”, choice = SOME (“$++”), fail = SOME (“[]”), guard =
      SOME (“assert”), ignorebind = SOME (“monad_unitbind”), unit =
      “λx. [x]”}),
    ("option",
     {bind = “monad_bind”, choice = SOME (“$++”), fail = SOME (“NONE”),
      guard = SOME (“assert”), ignorebind = SOME (“monad_unitbind”), unit =
      “SOME”})]: (string * monadsyntax.monadinfo) list

Everything that has been enabled can in turn be disabled, with calls drawn from:

     disable_monad            : string -> unit
     temp_disable_monad       : string -> unit

     disable_monadsyntax      : unit -> unit
     temp_disable_monadsyntax : unit -> unit

Some built-in monad theories

See Figure 5.7.3 for the bind definitions for a number of different monads that are present in the core HOL set of theories.

errorStateMonadTheory.BIND_DEF
  ⊢ ∀g f s0.
       errorStateMonad$BIND g f s0 =
       case g s0 of NONE => NONE | SOME (b,s) => f b s

listTheory.LIST_BIND_THM
  ⊢ LIST_BIND [] f = [] ∧
     LIST_BIND (h::t) f = APPEND (f h) (LIST_BIND t f)

optionTheory.OPTION_BIND_def
  ⊢ (∀f. OPTION_BIND NONE f = NONE) ∧
     ∀x f. OPTION_BIND (SOME x) f = f x

readerMonadTheory.BIND_def
  ⊢ ∀M f s. readerMonad$BIND M f s = f (M s) s

state_transformerTheory.BIND_DEF
  ⊢ ∀g f. state_transformer$BIND g f = UNCURRY f ∘ g

Other Theories

Other theories of interest in HOL are listed and briefly described in the table below.

TheoryDescription
posetPartial Orders, Knaster-Tarski theorem
divides, gcdDivisibility and the greatest common divisor
polyA theory of polynomials over $\mathbb{R}$, providing a collection of operations on polynomials, and theorems about them
Temporal_Logic, Omega_AutomataKlaus Schneider's development of temporal logic and $\omega$-automata
ctl, muComputation Tree Logic and the $\mu$-calculus. See Hasan Amjad's thesis
lbtreePossibly infinitely deep (i.e., co-algebraic) binary trees
inftreePossibly infinitely branching, algebraic trees

  1. To simplify the porting of the LCF theorem-proving tools to the HOL system, the HOL logic was made as like PP$\lambda$ (the logic built-in to LCF) as possible.

  2. Constants declared in new theories can freely re-use these names, with ambiguous inputs resolved by type inference.

  3. This theorem has an un-reduced $\beta$-redex in order to meet the interface required by the type definition principle.

  4. The definition of disjoint unions in the HOL system is due to Tom Melham. The technical details of this definition can be found in (Melham 1989).

  5. When using the parenthesis-version, the one value's syntax consists of two parenthesis tokens, so that one can write the value with white-space between the parentheses if desired.

  6. In higher order logic, primitive recursion is much more powerful than in first order logic; for example, Ackermann's function can be defined by primitive recursion in higher order logic.

  7. A set of numbers is downward closed if whenever it contains the successor of a number, it also contains the number.

  8. Note that, unlike the case of real numbers in HOL, $0/0 = 0$ (or “division by zero” in general) does not hold on extreals. This particular design choice sometimes makes proofs of extreal-related theorems a bit harder (but more aligned with their textbook proofs), as whenever terms like inv x or 1 / x are involved, x <> 0 must be proved to proceed.

  9. The current theory subsumes previous word theories — it evolved from a development based on an equivalence class construction. Wai Wong's word theory, which was based on Paul Curzon's rich_list theory, is no longer distributed with HOL. The principle advantages of the current theory are that there is just one theory for all word sizes and that word length side conditions are not required.

  10. The theory of finite Cartesian products was ported from HOL Light.

  11. Note that FCP indices in HOL Light are ranged from 1 to dimindex('b), while in HOL4 they are ranged from 0 to dimindex('b) - 1, thus is less than the size of 'b. Also note that the function fcp_index in HOL Light is specified for index values: f ' i = 0 when i = 0 or i > dimindex('b). In HOL4, however, f ' i is unspecified when i >= dimindex('b).

  12. Note that it is impossible to introduce words of length zero because all types must be inhabited, and hence their size will always be greater than or equal to one.

  13. Words are not tagged as being signed/unsigned. Mappings to/from the integers (w2i and i2w) are provided in the theory integer_word.

  14. Starting with the original definition it is not that easy to derive the coinduction principle, which is very useful in practice.

  15. The original idea is due to J Moore, who suggested it for use in ACL2.

  16. Indeed, writing do M od for a single binding form $M$ will see the system print back $M$ on its own.

Core Theories — Higher Mathematics

Ordinals and Cardinals in HOL

HOL provides the theory of ordinals and cardinals in wellorderTheory, cardinalTheory, and ordinalTheory, etc. (Norrish and Huffman 2013). Unfortunately, the typed logic implemented in the various HOL systems (including Isabelle/HOL) is not strong enough to define a type for all possible ordinal values. It turns out, however, that for any fixed $n \in \mathbb{N}$, we can model all ordinals of cardinality $\aleph_n$. The user is thus able to choose an ordinal domain of sufficient size for their purposes.

Ordinals in HOL

The ordinals are an extension of the natural numbers1. The idea is to add new “numbers” after the naturals. For example, if we add $\omega$, we get $0$, $1$, $2$, $\ldots$, $\omega$, where $0 < 1 < 2 < \cdots < \omega$. Notice that this new set extends the naturals and is countable. In addition, under this ordering it has a biggest element $\omega$, is linearly ordered, and well-founded. The ordinals are what you get if you keep adding elements. The first few ordinals are: $0$, $1$, $\ldots$, $\omega$, $\omega + 1$, $\ldots$, $\omega\times 2$. Notice that (the ordinals less than) $\omega\times 2$ is what you get if you put two copies of (the ordinals less than) $\omega$ next to each other, i.e., $0, 1, \ldots, 0, 1, \ldots$ (and then rename the elements in the second copy). Continuing to add “numbers”, we get $\omega$, $\omega\times 2$, $\omega\times 3$, $\ldots$, $\omega^2$. Now we have introduced ordinal exponentiation. Note that $\omega^2$ is what you get if you put $\omega$ copies of $\omega$, one after the other (and rename elements). We will pick up where we left off, but will now start extending the ordinals at a faster rate: $\omega^2$, $\omega^2+1$, $\ldots$, $\omega^2+\omega$, $\omega^2+\omega+1$, $\ldots$, $\omega^2+(\omega\times 2)$, $\omega^2+(\omega\times 2)+1$, $\ldots$, $\omega^2+(\omega\times 3)$, $\ldots$, $\omega^2+(\omega\times 4)$, $\ldots$, $\omega^2\times 2$, $\ldots$, $\omega^2\times 3$, $\ldots$, $\omega^3$, $\omega^4$, $\ldots$, $\omega^\omega$, $\ldots$, $\omega^{(\omega^\omega)}$, $\ldots$, $\omega^{(\omega^{(\omega^\omega)})}$, $\ldots$, $\epsilon_0$. The set of ordinals less than $\epsilon_0$ remain countable. The first ordinal with uncountably many predecessors is called $\omega_1$.

omega1_def
  ⊢ ω₁ = sup {a | countableOrd a}
x_lt_omega1_countable
  ⊢ x < ω₁ ⇔ countableOrd x

Cardinals in HOL

In mathematics, a cardinal number, or cardinal for short, is what is commonly called the number of elements of a set. In the case of a finite set $s$, its cardinal number, or cardinality is therefore a natural number, say $n$. Within pred_setTheory (Section 5.5.1), this is denoted by either by CARD s = n, or by s HAS_SIZE n. For dealing with the case of infinite sets, the infinite cardinal numbers have been introduced, which are often denoted with the Hebrew letter $\aleph$ marked with subscript indicating their rank among the infinite cardinals.

Currently HOL does not have cardinal arithmetic, such as those (Blanchette, Popescu, and Traytel 2014) available in Isabelle/HOL. Nevertheless, it is possible to compare the cardinalities of any two infinite sets. Cardinality is defined in terms of bijective functions:

cardleq_def
  ⊢ ∀s1 s2. s1 ≼ s2 ⇔ ∃f. INJ f s1 s2

Topology of One-Dimensional Euclidean Space

The theory real_topology provides some deeper analytical properties for real numbers and real sets (one-dimensional Euclidean space). It was ported from HOL Light by Muhammad Qasim and Osman Hasan et al. In particular, real_topology contains more general and advanced concepts and theorems than those provided by lim and seq (Section 5.3.6).

There are more than 60 concepts and 1600 theorems in real_topology, including properties of continuous functions and the completeness of real numbers, which is the foundation of other higher mathematics theories, such as differential and integral calculus, measure and probability theories. For instance, the following version of the Heine-Borel theorem (and the fact that a real set is compact iff it is bounded and closed) plays an essential role in the construction of Borel-measurable spaces in measure theory:

COMPACT_EQ_HEINE_BOREL
  ⊢ ∀s. compact s ⇔
         ∀f. (∀t. t ∈ f ⇒ open t) ∧ s ⊆ BIGUNION f ⇒
             ∃f'. f' ⊆ f ∧ FINITE f' ∧ s ⊆ BIGUNION f'

COMPACT_EQ_BOUNDED_CLOSED
  ⊢ ∀s. compact s ⇔ bounded s ∧ closed s

The concept of open (and closed) sets is fundamental and is needed, for example, in the definition of Borel sets (see Section 6.4.1):

open_def
  ⊢ ∀s. open s ⇔
         ∀x. x ∈ s ⇒ ∃e. 0 < e ∧ ∀x'. dist (x',x) < e ⇒ x' ∈ s

closed_def
  ⊢ ∀s. closed s ⇔ open (𝕌(:real) DIFF s)

Univariate Differential and Integral Calculus

HOL offers two groups of theories for univariate differential and integral calculus. The old one consists of limTheory (limits of functions, differentiation and continuity) and integralTheory (integration), while the new one consists of derivativeTheory and integrationTheory with fundamental concepts from real_topologyTheory. Both groups of theories were originally developed by John Harrison2.

These two group of theories are now compatible by having some bridging theorems showing the equivalences between their key concepts. The old theories align more tightly to conventional textbooks, while the new theories usually offer some extra functionalities. HOL users can use both of them within the same proof scripts. The rest of this section briefly introduce some important concepts and theorems, focusing on the bridges between old and new theories3.

Derivative

A quintessentially analytic property is the derivative of a (real-valued) function. The concept of derivative is first suggested by the intuitive notion of the tangent to a smooth curve $y = f(x)$ at a point $P$ with the coordinates $x$ and $y$. See Figure 6.3.1 for an illustration of this differentiation process. Mathematically, one shall generally use either the notation of Lagrange, $y' = f'(x)$, to denote the derivative, or, as Leibnitz did, the symbol $dy/dx$ or $df(x)/dx$. The notation $f'(x)$ indicates the fact that the derivative is itself a function of $x$ since a value of $f'(x)$ corresponds to each value of $x$ (in the interval considered). The definition of the derivative appears in several different forms, one of the following:

$$f'(x) = \lim_{h\rightarrow 0}\frac{f(x+h) - f(x)}{h}$$

One might expect to find a higher-order ‘derivative’ function deriv $\colon (\mathbb{R}\rightarrow\mathbb{R})\rightarrow(\mathbb{R}\rightarrow\mathbb{R})$ mapping a function $f$ to its derivative $f'$. Actually, no such thing is defined in HOL, but rather a ternary operator diffl (defined in limTheory) where (f diffl l) x means 'the function $f$ has a derivative with value $l$ at the point $x$'. Expanding out the definition, we see that the definition is much as an analyst might expect:

diffl
  ⊢ ∀f l x. (f diffl l) x ⇔ ((λh. (f (x + h) − f x) / h) -> l) 0
The differentiation process of (f diffl l) x
The differentiation process of (f diffl l) x

(f has_derivative f') (at x) means 'the function $f$ has a derivative $f'$ at the point $x$', or (f has_derivative f') (at x within s) means 'the function $f$ has a derivative $f'$ at the point $x$ within the set $s$'. Expanding out the definition, we see that the definition is much as an analyst might expect: (cf. Figure 6.3.1 for an illustration of this differentiation process.)

HAS_DERIVATIVE_AT
  ⊢ ∀f f' x.
       (f has_derivative f') (at x) ⇔
       linear f' ∧
       ∀e. 0 < e ⇒
           ∃d. 0 < d ∧
               ∀x'.
                 0 < abs (x' − x) ∧ abs (x' − x) < d ⇒
                 abs (f x' − f x − f' (x' − x)) / abs (x' − x) < e
The differentiation process of f has_derivative f'
The differentiation process of f has_derivative f'
has_vector_derivative
  ⊢ ∀f f' net.
       (f has_vector_derivative f') net ⇔ (f has_derivative (λx. x * f')) net

Similarly, (f has_vector_derivative f') (at x) means that the function $f$ has a derivative with value $f'$ at the point $x$. The usual kinds of combining theorems are proved. For example, the derivative of a sum of two functions is the sum of their derivatives:

DIFF_ADD
  ⊢ ∀f g l m x.
       (f diffl l) x ∧ (g diffl m) x ⇒ ((λx. f x + g x) diffl (l + m)) x

HAS_DERIVATIVE_ADD
  ⊢ ∀f f' g g' net.
       (f has_derivative f') net ∧ (g has_derivative g') net ⇒
       ((λx. f x + g x) has_derivative (λh. f' h + g' h)) net

DIFF_MUL
  ⊢ ∀f g l m x.
       (f diffl l) x ∧ (g diffl m) x ⇒
       ((λx. f x * g x) diffl (l * g x + m * f x)) x

HAS_DERIVATIVE_MUL_AT
  ⊢ ∀f f' g g' a.
       (f has_derivative f') (at a) ∧ (g has_derivative g') (at a) ⇒
       ((λx. f x * g x) has_derivative (λh. f a * g' h + f' h * g a)) (at a)

DIFF_DIV
  ⊢ ∀f g l m x.
       (f diffl l) x ∧ (g diffl m) x ∧ g x ≠ 0 ⇒
       ((λx. f x / g x) diffl ((l * g x − m * f x) / (g x)²)) x

DIFF_CHAIN
  ⊢ ∀f g l m x.
       (f diffl l) (g x) ∧ (g diffl m) x ⇒ ((λx. f (g x)) diffl (l * m)) x

limTheory.MVT
  ⊢ ∀f a b.
       a < b ∧ (∀x. a ≤ x ∧ x ≤ b ⇒ f contl x) ∧
       (∀x. a < x ∧ x < b ⇒ f differentiable x) ⇒
       ∃l z. a < z ∧ z < b ∧ (f diffl l) z ∧ f b − f a = (b − a) * l

derivativeTheory.MVT
  ⊢ ∀f f' a b.
       a < b ∧ f continuous_on interval [(a,b)] ∧
       (∀x. x ∈ interval (a,b) ⇒ (f has_derivative f' x) (at x)) ⇒
       ∃x. x ∈ interval (a,b) ∧ f b − f a = f' x (b − a)

diffl_has_vector_derivative
  ⊢ ∀f l x. (f diffl l) x ⇔ (f has_vector_derivative l) (at x)

A function is differentiable at certain point if it has derivative at that point.

The continuity of a function is very close with its differentiability. The HOL formalization of "$f$ is (right-)continuous at the point $x$" is f contl x with contl ('locally continuous' from limTheory), or f continuous (at x) from real_topologyTheory, both are infix binary operators. The two operators are equivalent (when continuous is used with the at net) by the following bridging theorem:

contl_eq_continuous_at
  ⊢ ∀f x. f contl x ⇔ f continuous at x

However, using continuous it is also possible to express continuity of a function at a point within a set of points (usually an interval), i.e. f continuous (at x within s).

The global version of continuous for a set of points (usually an open or closed interval) is called continuous_on.4

continuous_on
  ⊢ ∀f s.
       f continuous_on s ⇔
       ∀x. x ∈ s ⇒
           ∀e. 0 < e ⇒
               ∃d. 0 < d ∧
                   ∀x'. x' ∈ s ∧ dist (x',x) < d ⇒ dist (f x',f x) < e

CONTINUOUS_ON_EQ_CONTINUOUS_AT
  ⊢ ∀f s. open s ⇒ (f continuous_on s ⇔ ∀x. x ∈ s ⇒ f continuous at x)

CONTINUOUS_ON_EQ_CONTINUOUS_WITHIN
  ⊢ ∀f s. f continuous_on s ⇔ ∀x. x ∈ s ⇒ f continuous (at x within s)

One of the most fundamental facts about continuous functions is the intermediate value property (IVT): if a function takes two values then it also somewhere takes any value in between. The HOL formulation is that if $f$ is continuous at all points of an interval $[a,b]$, and $f(a)\leqslant y\leqslant f(b)$ then there is some point $x$ in the interval where $f(x) = y$:

IVT
  ⊢ ∀f a b y.
       a ≤ b ∧ (f a ≤ y ∧ y ≤ f b) ∧ (∀x. a ≤ x ∧ x ≤ b ⇒ f contl x) ⇒
       ∃x. a ≤ x ∧ x ≤ b ∧ f x = y

CONTINUOUS_ON_IVT
  ⊢ ∀f a b y.
       a ≤ b ∧ f a ≤ y ∧ y ≤ f b ∧ f continuous_on interval [(a,b)] ⇒
       ∃x. x ∈ interval [(a,b)] ∧ f x = y

IVT2
  ⊢ ∀f a b y.
       a ≤ b ∧ (f b ≤ y ∧ y ≤ f a) ∧ (∀x. a ≤ x ∧ x ≤ b ⇒ f contl x) ⇒
       ∃x. a ≤ x ∧ x ≤ b ∧ f x = y

Integration

The version of integral calculus formalized in HOL is a generalized Riemann integration called Henstock-Kurzweil Integration, or gauge integration (Bartle 2001). Many functions which are not Riemann integrable can be shown to be Henstock-Kurzweil integrable. (In particular, a function is Lebesgue integrable if and only if the function and its absolute value are Henstock-Kurzweil integrable.)

f integrable_on s means that the function $f$ is integrable on a real set $s$ (there is another similar concept called absolute integrability: f absolutely_integrable_on s.) If $f$ is integrable on a closed interval $[a,b]$, then the usual integration of $f$ on $[a,b]$, i.e. $\displaystyle\int_a^b f \,\mathrm{d}x$, is denoted by integral (interval [a,b]) f in HOL. In general, the integration of $f$ on a set $s$ of real numbers is integral s f.

gauge
  ⊢ ∀E g. gauge E g ⇔ ∀x. E x ⇒ 0 < g x

fine
  ⊢ ∀g D p. fine g (D,p) ⇔ ∀n. n < dsize D ⇒ D (SUC n) − D n < g (p n)

tdiv
  ⊢ ∀a b D p.
       tdiv (a,b) (D,p) ⇔ division (a,b) D ∧ ∀n. D n ≤ p n ∧ p n ≤ D (SUC n)

division
  ⊢ ∀a b D.
       division (a,b) D ⇔
       D 0 = a ∧ ∃N. (∀n. n < N ⇒ D n < D (SUC n)) ∧ ∀n. n ≥ N ⇒ D n = b

Dint
  ⊢ ∀a b f k.
       Dint (a,b) f k ⇔
       ∀e. 0 < e ⇒
           ∃g. gauge (λx. a ≤ x ∧ x ≤ b) g ∧
               ∀D p.
                 tdiv (a,b) (D,p) ∧ fine g (D,p) ⇒ abs (rsum (D,p) f − k) < e

integral
  ⊢ ∀a b f. integral (a,b) f = @i. Dint (a,b) f i

In integration, ported from HOL Light, a more general form of Henstock-Kurzweil integration has been defined:

gauge_def
  ⊢ ∀d. gauge d ⇔ ∀x. x ∈ d x ∧ open (d x)

FINE
  ⊢ ∀d s. d FINE s ⇔ ∀x k. (x,k) ∈ s ⇒ k ⊆ d x

tagged_partial_division_of
  ⊢ ∀s i.
       s tagged_partial_division_of i ⇔
       FINITE s ∧
       (∀x k. (x,k) ∈ s ⇒ x ∈ k ∧ k ⊆ i ∧ ∃a b. k = interval [(a,b)]) ∧
       ∀x1 k1 x2 k2.
         (x1,k1) ∈ s ∧ (x2,k2) ∈ s ∧ (x1,k1) ≠ (x2,k2) ⇒
         interior k1 ∩ interior k2 = ∅

tagged_division_of
  ⊢ ∀s i.
       s tagged_division_of i ⇔
       s tagged_partial_division_of i ∧ BIGUNION {k | (∃x. (x,k) ∈ s)} = i

has_integral_compact_interval
  ⊢ ∀f y i.
       (f has_integral_compact_interval y) i ⇔
       ∀e. 0 < e ⇒
           ∃d. gauge d ∧
               ∀p. p tagged_division_of i ∧ d FINE p ⇒
                   abs (sum p (λ(x,k). content k * f x) − y) < e

has_integral_def
  ⊢ ∀f y i.
       (f has_integral y) i ⇔
       if ∃a b. i = interval [(a,b)] then
         (f has_integral_compact_interval y) i
       else
         ∀e. 0 < e ⇒
             ∃B. 0 < B ∧
                 ∀a b.
                   ball (0,B) ⊆ interval [(a,b)] ⇒
                   ∃z. ((λx. if x ∈ i then f x else 0) has_integral_compact_interval
                        z) (interval [(a,b)]) ∧ abs (z − y) < e

integral_def
  ⊢ ∀i f. integral i f = @y. (f has_integral y) i

The fundamental theorem of calculus (in the equivalent forms of Riemann integral) is expressed below:

FTC1
  ⊢ ∀f f' a b.
       a ≤ b ∧ (∀x. a ≤ x ∧ x ≤ b ⇒ (f diffl f' x) x) ⇒
       Dint (a,b) f' (f b − f a)

FUNDAMENTAL_THEOREM_OF_CALCULUS
  ⊢ ∀f f' a b.
       a ≤ b ∧
       (∀x. x ∈ interval [(a,b)] ⇒
            (f has_vector_derivative f' x) (at x within interval [(a,b)])) ⇒
       (f' has_integral f b − f a) (interval [(a,b)])

The above theorem requires that the function $f$ must be everywhere differentiable (i.e., has derivative) in the closed interval $[a,b]$. In fact, Henstock-Kurzweil Integration permits a countable number of exceptional points. This yields a significant improvement, as shown in the following strong version of the fundamental theorem of calculus:

FUNDAMENTAL_THEOREM_OF_CALCULUS_STRONG
  ⊢ ∀f f' s a b.
       countable s ∧ a ≤ b ∧ f continuous_on interval [(a,b)] ∧
       (∀x. x ∈ interval [(a,b)] DIFF s ⇒
            (f has_vector_derivative f' x) (at x within interval [(a,b)])) ⇒
       (f' has_integral f b − f a) (interval [(a,b)])

Finally, the following two bridging theorems can be used to convert between integrals of the integral and integration theories:

integral_old_to_new
  ⊢ ∀f a b.
       a ≤ b ∧ integrable (a,b) f ⇒
       integral (a,b) f = integral (interval [(a,b)]) f

integral_new_to_old
  ⊢ ∀f a b.
       a ≤ b ∧ f integrable_on interval [(a,b)] ⇒
       integral (interval [(a,b)]) f = integral (a,b) f

Measure Theory

The theme of measure theory implemented in sigma_algebra and measure theories is the problem of how to assign a size, a content, a probability, etc. to certain sets. The most common examples of measure is the length (in one dimension), area (in two dimensions) and volume (in three dimensions) in Euclidean spaces. In HOL's measure theory, a measure is a set function of the type $(\alpha \to \konst{bool}) \to \konst{extreal}$.5 (See Section 5.3.7 for more details of extended real numbers.) This allows us to express, for example, the “length” of the entire real line $\mathbb{R}$, i.e. PosInf.

A reasonable measure should be able to deal with disjoint countable partitions of sets. Therefore a measure function must be defined on a system of sets which is stable whenever we repeat any of the basic set operations -- $\cup$, $\cap$, ${}^{\mathrm{c}}$ -- countably many times, i.e. $\sigma$-algebra. In HOL, a system of sets is a pair (sp,sts) of the type $(\alpha \to \konst{bool}) \,\#\, ((\alpha \to \konst{bool}) \to \konst{bool})$ satisfying subset_class sp sts:

subset_class_def
  ⊢ ∀sp sts. subset_class sp sts ⇔ ∀x. x ∈ sts ⇒ x ⊆ sp

Given any system of sets a = (sp,sts), the two functions space and subsets can be used to access sp and sts, respectively.

Then algebra and $\sigma$-algebra6 are defined below:

algebra_def
  ⊢ ∀a. algebra a ⇔
         subset_class (space a) (subsets a) ∧ ∅ ∈ subsets a ∧
         (∀s. s ∈ subsets a ⇒ space a DIFF s ∈ subsets a) ∧
         ∀s t. s ∈ subsets a ∧ t ∈ subsets a ⇒ s ∪ t ∈ subsets a

sigma_algebra_def
  ⊢ ∀a. sigma_algebra a ⇔
         algebra a ∧ ∀c. countable c ∧ c ⊆ subsets a ⇒ BIGUNION c ∈ subsets a

There are several equivalent ways of defining $\sigma$-algebra. The following alternative definition (as a theorem), for instance, perfectly matches Definition 3.1 of (Schilling 2017):

SIGMA_ALGEBRA_ALT_SPACE
  ⊢ ∀a. sigma_algebra a ⇔
         subset_class (space a) (subsets a) ∧ space a ∈ subsets a ∧
         (∀s. s ∈ subsets a ⇒ space a DIFF s ∈ subsets a) ∧
         ∀f. f ∈ (𝕌(:num) → subsets a) ⇒
             BIGUNION (IMAGE f 𝕌(:num)) ∈ subsets a

Any measure (function) must be defined with respect to a measure space. In HOL, a measure space is represented by a tuple (sp,sts,mu), where sp and sts forms a $\sigma$-algebra7, and mu is the measure function. If m = (sp,sts,mu) is a measure space, the functions m_space, measurable_sets and measure can be used to access sp, sts and mu, respectively. Any measure space must be both positive and countably_additive:

measure_space_def
  ⊢ ∀m. measure_space m ⇔
         sigma_algebra (measurable_space m) ∧ positive m ∧
         countably_additive m

positive_def
  ⊢ ∀m. positive m ⇔
         measure m ∅ = 0 ∧ ∀s. s ∈ measurable_sets m ⇒ 0 ≤ measure m s

countably_additive_def
  ⊢ ∀m. countably_additive m ⇔
         ∀f. f ∈ (𝕌(:num) → measurable_sets m) ∧
             (∀i j. i ≠ j ⇒ DISJOINT (f i) (f j)) ∧
             BIGUNION (IMAGE f 𝕌(:num)) ∈ measurable_sets m ⇒
             measure m (BIGUNION (IMAGE f 𝕌(:num))) = suminf (measure m ∘ f)

In particular, if (sp,sts,mu) is positive and countably_additive but (sp,sts) is not (yet) a $\sigma$-algebra, it is called a premeasure (space):

premeasure_def
  ⊢ ∀m. premeasure m ⇔ positive m ∧ countably_additive m

The reason of introducing premeasure is that it is not a trivial task to assign explicitly a value to every set from a $\sigma$-algebra. Rather than doing this it is often more natural to assign values to sets from some generators. To get such an automatic extension the following (technically motivated) class of generators is useful. A semiring (usually denoted by $\mathscr{S}$) is a family with the following properties:

sigma_algebraTheory.semiring_def
  ⊢ ∀r. semiring r ⇔
         subset_class (space r) (subsets r) ∧ ∅ ∈ subsets r ∧
         (∀s t. s ∈ subsets r ∧ t ∈ subsets r ⇒ s ∩ t ∈ subsets r) ∧
         ∀s t.
           s ∈ subsets r ∧ t ∈ subsets r ⇒
           ∃c. c ⊆ subsets r ∧ FINITE c ∧ disjoint c ∧ s DIFF t = BIGUNION c

Any ($\sigma$-)algebra is also a semiring but not vice versa. For instance, the set of all right open intervals $\big\{ [a,b) \colon a,b\in\mathbb{R} \big\}$ is a semiring (because the set difference of any two right open intervals is at most two new right open intervals). Furthermore, for any system of sets (sp sts) there exists a smallest $\sigma$-algebra containing it (also called the $\sigma$-algebra generated from it), denoted by $\sigma(\cdot)$:

sigma_def
  ⊢ ∀sp sts.
       sigma sp sts = (sp,BIGINTER {s | sts ⊆ s ∧ sigma_algebra (sp,s)})

SIGMA_ALGEBRA_SIGMA
  ⊢ ∀sp sts. subset_class sp sts ⇒ sigma_algebra (sigma sp sts)

SIGMA_SUBSET_SUBSETS
  ⊢ ∀sp a. a ⊆ subsets (sigma sp a)

The following deep extension theorem for measures goes back to Carathéodory (Schilling 2017):

CARATHEODORY_SEMIRING
  ⊢ ∀m0.
       semiring (measurable_space m0) ∧ premeasure m0 ⇒
       ∃m. (∀s. s ∈ measurable_sets m0 ⇒ measure m s = measure m0 s) ∧
           measurable_space m = sigma (m_space m0) (measurable_sets m0) ∧
           measure_space m

In addition, the uniqueness of measures extended from generators is guaranteed by the following theorem, given that the involved generators are stable under $\cap$ and $\sigma$-finite:

sigma_finite_def
  ⊢ ∀m. sigma_finite m ⇔
         ∃f. f ∈ (𝕌(:num) → measurable_sets m) ∧ (∀n. f n ⊆ f (SUC n)) ∧
             BIGUNION (IMAGE f 𝕌(:num)) = m_space m ∧
             ∀n. measure m (f n) < +∞

sigma_finite_measure_space_def
  ⊢ ∀m. sigma_finite_measure_space m ⇔ measure_space m ∧ sigma_finite m

UNIQUENESS_OF_MEASURE
  ⊢ ∀sp sts u v.
       subset_class sp sts ∧ (∀s t. s ∈ sts ∧ t ∈ sts ⇒ s ∩ t ∈ sts) ∧
       sigma_finite (sp,sts,u) ∧
       measure_space (sp,subsets (sigma sp sts),u) ∧
       measure_space (sp,subsets (sigma sp sts),v) ∧
       (∀s. s ∈ sts ⇒ u s = v s) ⇒
       ∀s. s ∈ subsets (sigma sp sts) ⇒ u s = v s

Borel measurable function and sets

The theory real_borel provides Borel sets and Borel measurable functions based on reals, in addition the theory borel provides the same things based on extreals, plus the constructions of Borel and Lebesgue measure spaces.

In HOL, the real-valued Borel sets borel $\mathscr{B}(\mathbb{R})$ is the $\sigma$-algebra generated by open sets (of the type $\konst{real} \to \konst{bool}$).

borel
  ⊢ borel = sigma 𝕌(:real) {s | open s}

borel can also be generated by half spaces, open or closed intervals:

borel_eq_le
  ⊢ borel = sigma 𝕌(:real) (IMAGE (λa. {x | x ≤ a}) 𝕌(:real))
borel_eq_less
  ⊢ borel = sigma 𝕌(:real) (IMAGE (λa. {x | x < a}) 𝕌(:real))
borel_eq_gr
  ⊢ borel = sigma 𝕌(:real) (IMAGE (λa. {x | a < x}) 𝕌(:real))
borel_eq_gr_le
  ⊢ borel =
     sigma 𝕌(:real) (IMAGE (λ(a,b). {x | a < x ∧ x ≤ b}) 𝕌(:real # real))
borel_eq_gr_less
  ⊢ borel =
     sigma 𝕌(:real) (IMAGE (λ(a,b). {x | a < x ∧ x < b}) 𝕌(:real # real))
borel_eq_ge_le
  ⊢ borel =
     sigma 𝕌(:real) (IMAGE (λ(a,b). {x | a ≤ x ∧ x ≤ b}) 𝕌(:real # real))
borel_eq_ge_less
  ⊢ borel =
     sigma 𝕌(:real) (IMAGE (λ(a,b). {x | a ≤ x ∧ x < b}) 𝕌(:real # real))

The extreal-valued Borel sets Borel $\mathscr{B}(\mathbb{\overline{R}})$ is defined by augmenting borel with infinities:

Borel
  ⊢ Borel =
     (𝕌(:extreal),
      {B' |
       ∃B S.
         B' = IMAGE Normal B ∪ S ∧ B ∈ subsets borel ∧
         S ∈ {∅; {−∞}; {+∞}; {−∞; +∞}}})

Borel can also be generated by half spaces (of the type $\konst{extreal} \to \konst{bool}$):8

Borel_def
  ⊢ Borel = sigma 𝕌(:extreal) (IMAGE (λa. {x | x < Normal a}) 𝕌(:real))
Borel_eq_le
  ⊢ Borel = sigma 𝕌(:extreal) (IMAGE (λa. {x | x ≤ Normal a}) 𝕌(:real))
Borel_eq_gr
  ⊢ Borel = sigma 𝕌(:extreal) (IMAGE (λa. {x | Normal a < x}) 𝕌(:real))
Borel_eq_ge
  ⊢ Borel = sigma 𝕌(:extreal) (IMAGE (λa. {x | Normal a ≤ x}) 𝕌(:real))

Let a and b be two measurable spaces ($\sigma$-algebras). A map $f \colon \konst{space}\,a \to \konst{space}\,b$ is called $a/b$-measurable (or measurable unless this is too ambiguous) if the pre-image of every measurable set is a measurable set: (the definition is in sigma_algebra theory)

measurable_def
  ⊢ ∀a b.
       measurable a b =
       {f |
        f ∈ (space a → space b) ∧
        ∀s. s ∈ subsets b ⇒ PREIMAGE f s ∩ space a ∈ subsets a}

A function is called Borel-measurable if it is a measurable map from any measurable space to borel or Borel:

val _ = overload_on ("borel_measurable", ``\a. measurable a borel``);
val _ = overload_on ("Borel_measurable", ``\a. measurable a Borel``);

For instance, a random variable is nothing but a measurable map from a probability space (which is also a measure space, see Section 6.6) to Borel. Thus in HOL there are actually two different types of Borel-measurable functions, real-valued or extreal-valued, each with a large set of theorems for proving the Borel-measurabilities, in real_borel and borel theories, respectively. For instance, the addition or subtraction of two real-valued (or extreal-valued) Borel-measurable functions $f$ and $g$ is still Borel-measurable:

in_borel_measurable_add
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ borel_measurable a ∧ g ∈ borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x + g x) ⇒
       h ∈ borel_measurable a

IN_MEASURABLE_BOREL_ADD'
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ Borel_measurable a ∧ g ∈ Borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x + g x) ⇒
       h ∈ Borel_measurable a

in_borel_measurable_sub
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ borel_measurable a ∧ g ∈ borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x − g x) ⇒
       h ∈ borel_measurable a

IN_MEASURABLE_BOREL_SUB'
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ Borel_measurable a ∧ g ∈ Borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x − g x) ⇒
       h ∈ Borel_measurable a

in_borel_measurable_mul
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ borel_measurable a ∧ g ∈ borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x * g x) ⇒
       h ∈ borel_measurable a

IN_MEASURABLE_BOREL_TIMES'
  ⊢ ∀a f g h.
       sigma_algebra a ∧ f ∈ Borel_measurable a ∧ g ∈ Borel_measurable a ∧
       (∀x. x ∈ space a ⇒ h x = f x * g x) ⇒
       h ∈ Borel_measurable a

Furthermore, if a real-valued function is Borel-measurable, so is its extreal-valued version. The converse also holds.

IN_MEASURABLE_BOREL_IMP_BOREL'
  ⊢ ∀a f.
       sigma_algebra a ∧ f ∈ borel_measurable a ⇒
       Normal ∘ f ∈ Borel_measurable a

in_borel_measurable_from_Borel
  ⊢ ∀a f.
       sigma_algebra a ∧ f ∈ Borel_measurable a ⇒
       real ∘ f ∈ borel_measurable a

Construction of Borel measure space (lborel)

The Borel measure space lborel (of the type $(\konst{real} \to \konst{bool}) \,\#\, ((\konst{real} \to \konst{bool}) \to \konst{bool}) \,\#\, ((\konst{real} \to \konst{bool}) \to \konst{extreal})$) is the household measure space defined on the real-valued Borel sets (borel) such that any interval with end points $a,b \in \mathbb{R}$ ($a \leqslant b$) has the extreal-valued measure of $b - a \in \mathbb{\overline{R}}$. The construction of lborel is done by extending a premeasure in the following steps:

  1. Define the system of all right-open intervals and a “premeasure” lambda0 on it:

    right_open_interval
      ⊢ ∀a b. right_open_interval a b = {x | a ≤ x ∧ x < b}
    
    right_open_intervals
      ⊢ right_open_intervals = (𝕌(:real),{right_open_interval a b | T})
    
    lambda0_def
      ⊢ ∀a b. a ≤ b ⇒ lambda0 (right_open_interval a b) = Normal (b − a)
    
  2. Prove that the set of all right open intervals is a semiring and the $\sigma$-algebra generated from it is nothing but borel:

    right_open_intervals_semiring
      ⊢ semiring right_open_intervals
    
    right_open_intervals_sigma_borel
      ⊢ sigma (space right_open_intervals) (subsets right_open_intervals) =
         borel
    
  3. Prove (space right_open_intervals, subsets right_open_intervals, lambda0) (abbreviated as lborel0) is additive, finite_additive and eventually a premeasure (positive and countably_additive):

    lborel0_additive
      ⊢ additive lborel0
    
    lborel0_finite_additive
      ⊢ finite_additive lborel0
    
    lborel0_premeasure
      ⊢ premeasure lborel0
    

    (The proof of lborel0_premeasure depends on real_topology theory, especially the Heine-Borel Theorem. See Section 6.2.)

  4. By Carathéodory Extension Theorem (CARATHEODORY_SEMIRING, see Section 6.4), there exists an extension of lborel0, denoted by lborel, which is now a measure space:

    lborel_def
      ⊢ (∀s. s ∈ subsets right_open_intervals ⇒ lambda s = lambda0 s) ∧
         measurable_space lborel = borel ∧ measure_space lborel
    

Let lambda = measure lborel be an abbreviation, the measure of the following typical Borel sets are proved as theorems:

lambda_prop
  ⊢ ∀a b. a ≤ b ⇒ lambda (right_open_interval a b) = Normal (b − a)

lambda_sing
  ⊢ ∀c. lambda {c} = 0

lambda_open_interval
  ⊢ ∀a b. a ≤ b ⇒ lambda (interval (a,b)) = Normal (b − a)

lambda_closed_interval
  ⊢ ∀a b. a ≤ b ⇒ lambda (interval [(a,b)]) = Normal (b − a)

Furthermore, the extreal-based Borel measurable space (ext_lborel) is defined by:

ext_lborel_def
  ⊢ ext_lborel = (space Borel,subsets Borel,lambda ∘ real_set)

real_set_def
  ⊢ ∀s. real_set s = {real x | x ≠ +∞ ∧ x ≠ −∞ ∧ x ∈ s}

where real_set (extreal theory) converts a set of extreals to the corresponding set of reals, filtering out $\pm\infty$. It can be proven that ext_lborel is indeed a $\sigma$-finite measure space:

measure_space_ext_lborel
  ⊢ measure_space ext_lborel

sigma_finite_ext_lborel
  ⊢ sigma_finite ext_lborel

Construction of Lebesgue measure space (lebesgue)

It is well known that lborel is not complete, in the sense that if for non-empty set s we have lambda s = 0, it's not true that all subsets of s has also the measure 0, as some of them may not be Borel sets at all. The completion of lborel is called Lebesgue measure space, denoted by lebesgue. In extreal theory (at $(HOLDIR)/examples/probability directory), it is defined directly by Henstock-Kurzweil Integration (see Section 6.3.2):

lebesgue_def
  ⊢ lebesgue =
     (𝕌(:real),{A | ∀n. indicator A integrable_on line n},
      (λA. sup {Normal (integral (line n) (indicator A)) | n ∈ 𝕌(:num)}))

real_borelTheory.line
  ⊢ ∀n. line n = {x | -&n ≤ x ∧ x ≤ &n}

The above definition indeed defines a $\sigma$-algebra and measure space. The Lebesgue measure of closed intervals is also proven (directly by integration theory):

sigma_algebra_lebesgue
  ⊢ sigma_algebra (𝕌(:real),{A | ∀n. indicator A integrable_on line n})

measure_space_lebesgue
  ⊢ measure_space lebesgue

lebesgue_closed_interval
  ⊢ ∀a b. a ≤ b ⇒ lmeasure (interval [(a,b)]) = Normal (b − a)

Now comes the relationship between lborel and lebesgue, which have two very different definitions. In fact, all Borel-measurable sets are also Lebesgue-measurable sets:

borel_imp_lebesgue_sets
  ⊢ ∀s. s ∈ subsets borel ⇒ s ∈ measurable_sets lebesgue

Finally, by uniqueness of measure, the two measure spaces lborel and lebesgue coincide on Borel sets (recall that lambda is an abbreviation of measure lborel):

lebesgue_eq_lambda
  ⊢ ∀s. s ∈ subsets borel ⇒ lmeasure s = lambda s

Lebesgue Integration Theory

The fundamental idea of integration is to measure the area between the graph of a function and the abscissa (i.e. $x$-axis). Riemann's approach partitions the domain of the function without taking into account the shape of the function, thus slicing up the area under the function vertically. Lebesgue's approach is exactly the opposite: the domain is partitioned according to the values of the function at hand, leading to a horizontal decomposition of the area.

In case the values of the function $f$ is finite, its domain can also be decomposed into the same number of disjoint sets (but each of them may not be a continuous interval). If the function is also nonnegative, i.e. $\forall x.\,0 \leqslant f(x)$, this function is called a positive simple function which can be also represented by a finite sum:

$$f = \sum_{j=0}^M y_j \mathbf{1}_{A_j}, \quad \text{where $\forall x\in A_j.\,f(x) = y_j$.}\quad\quad (\text{eq:posSimpleFn})$$

And the “integration” of $f$ is simply $\sum_{j=0}^M y_j\mu(A_j)$, where $\mu$ is the measure function of a measure space, e.g. lborel and lebesgue constructed in Section 6.4.1. Note that the representations of positive simple functions are not unique, but each representation can be uniquely determined by an index set $J$, a disjoint set of sets $A_j, j \in J$ and the set of values $y_j$ corresponding to each $A_j$. In HOL, the connection between positive simple functions (extreal-valued) and their representations is captured by pos_simple_fn, while the integration of positive simple functions is defined by pos_simple_fn_integral:

pos_simple_fn_def
  ⊢ ∀m f s a x.
       pos_simple_fn m f s a x ⇔
       (∀t. t ∈ m_space m ⇒ 0 ≤ f t) ∧
       (∀t. t ∈ m_space m ⇒ f t = ∑ (λi. Normal (x i) * 𝟙 (a i) t) s) ∧
       (∀i. i ∈ s ⇒ a i ∈ measurable_sets m) ∧ FINITE s ∧
       (∀i. i ∈ s ⇒ 0 ≤ x i) ∧
       (∀i j. i ∈ s ∧ j ∈ s ∧ i ≠ j ⇒ DISJOINT (a i) (a j)) ∧
       BIGUNION (IMAGE a s) = m_space m

pos_simple_fn_integral_def
  ⊢ ∀m s a x.
       pos_simple_fn_integral m s a x =
       ∑ (λi. Normal (x i) * measure m (a i)) s

Since the representations of a positive simple function are not unique, for each positive simple function there exists a set of representations (psfs), and a set of integrations (psfis) (although it can be proved that all these integrations are the same). The integration of an arbitrary positive function $f$ is defined as the supremum of all positive simple functions $g$ which is pointwise smaller than $f$:

pos_fn_integral_def
  ⊢ ∀m f.
       ∫⁺ m f = sup {r | (∃g. r ∈ psfis m g ∧ ∀x. x ∈ m_space m ⇒ g x ≤ f x)}

psfs_def
  ⊢ ∀m f. psfs m f = {(s,a,x) | pos_simple_fn m f s a x}

psfis_def
  ⊢ ∀m f.
       psfis m f =
       IMAGE (λ(s,a,x). pos_simple_fn_integral m s a x) (psfs m f)

For any positive function $f$, it is possible to construct a pointwise increasing sequence of positive simple functions $f_i$ which is always pointwise smaller than $f$. In another words, each $f(x)$ is the limit of $f_i(x)$ which is mono-increasing with $i$. Then the following monotone convergence theorem (actually due to Beppo Levi, see (Schilling 2017, 75)) reduces the integration of $f$ to the limit of integrations of $f_i$:

lebesgue_monotone_convergence
  ⊢ ∀m f fi.
       measure_space m ∧ (∀i. fi i ∈ Borel_measurable (measurable_space m)) ∧
       (∀i x. x ∈ m_space m ⇒ 0 ≤ fi i x) ∧
       (∀x. x ∈ m_space m ⇒ mono_increasing (λi. fi i x)) ∧
       (∀x. x ∈ m_space m ⇒ sup (IMAGE (λi. fi i x) 𝕌(:num)) = f x) ⇒
       ∫⁺ m f = sup (IMAGE (λi. ∫⁺ m (fi i)) 𝕌(:num))

Finally, the (Lebesgue) integration of an arbitrary function $f$ is defined by the subtraction of integrations of its positive and negative parts:

integral_def
  ⊢ ∀m f. ∫ m f = ∫⁺ m f⁺ − ∫⁺ m f⁻

extrealTheory.fn_plus_def
  ⊢ ∀f. f⁺ = (λx. if 0 < f x then f x else 0)

extrealTheory.fn_minus_def
  ⊢ ∀f. f⁻ = (λx. if f x < 0 then -f x else 0)

Note that it is possible that the two integrations above both result in the PosInf value, giving the entire integration an unspecified value (see Section 5.3.7 for more details on extreal arithmetics). A function $f$ is (Lebesgue) integrable if it is Borel-measurable and the integrations of its positive and negative parts are both finite (i.e. not $+\infty$):

integrable_def
  ⊢ ∀m f.
       integrable m f ⇔
       f ∈ Borel_measurable (measurable_space m) ∧ ∫⁺ m f⁺ ≠ +∞ ∧
       ∫⁺ m f⁻ ≠ +∞

A large set of theorems are provided in lebesgue theory on the properties of integrable functions and their integrations, including the following famous Markov's inequality:

markov_inequality
  ⊢ ∀m f a c.
       measure_space m ∧ integrable m f ∧ a ∈ measurable_sets m ∧ 0 < c ⇒
       measure m ({x | c ≤ abs (f x)} ∩ a) ≤
       c⁻¹ * ∫ m (λx. abs (f x) * 𝟙 a x)

Null sets and ‘almost everywhere’

A measurable set is called null set (with respect to its measure space) if its measure is zero:

null_set_def
  ⊢ ∀m s. null_set m s ⇔ s ∈ measurable_sets m ∧ measure m s = 0

A property $P(x)$ holds almost everywhere (AE) in a measure space $m$, denoted by AE x::m. P x in HOL9, if $P(x)$ holds for every $x$ excluding a null set:

borelTheory.AE_DEF
  ⊢ ∀m P. (AE x::m. P x) ⇔ ∃N. null_set m N ∧ ∀x. x ∈ m_space m DIFF N ⇒ P x

‘Almost everywhere’ is the basis of AE convergence in probability theory (see the next section). The following two theorems about the integration on null sets are very useful (Theorem 11.2 of (Schilling 2017, 89–90)):

integral_abs_eq_0
  ⊢ ∀m f.
       measure_space m ∧ f ∈ Borel_measurable (measurable_space m) ⇒
       (∫ m (abs ∘ f) = 0 ⇔ AE x::m. (abs ∘ f) x = 0) ∧
       ((AE x::m. (abs ∘ f) x = 0) ⇔
        measure m {x | x ∈ m_space m ∧ f x ≠ 0} = 0)

integral_null_set
  ⊢ ∀m f N.
       measure_space m ∧ f ∈ Borel_measurable (measurable_space m) ∧
       N ∈ null_set m ⇒
       integrable m (λx. f x * 𝟙 N x) ∧ ∫ m (λx. f x * 𝟙 N x) = 0

Radon-Nikodým theorem

So far there is no concept “derivative” being discussed. In Lebesgue integration, Radon-Nikodým (RN) Theorem (see, e.g., (Schilling 2017, 230) or (Fitzpatrick and Royden 2010, 382)) has the position of “Fundamental Theorem of Calculus” as in Riemann Integration. Recall in Lebesgue's theory one starts directly from the concept “integration” without defining “derivative” first. It is RN theorem which introduces the concept of “derivative” for Lebesgue integration and asserts its existence. The theorem says that for any $\sigma$-finite measure space $m$, if there is another measure $v$ absolutely continuous with respect to $m$, i.e. whenever any set $s$ has zero measure in measure space $m$ it also has same zero measure in $v$:

measure_absolutely_continuous_def
  ⊢ ∀v m. v ≪ m ⇔ ∀s. s ∈ measurable_sets m ∧ measure m s = 0 ⇒ v s = 0

Then there exists a nonnegative Borel-measurable function $f$ such that the (positive) integral of $f$ on any measurable set $s$ is exactly $v(s)$:

Radon_Nikodym
  ⊢ ∀m v.
       measure_space m ∧ sigma_finite m ∧
       measure_space (m_space m,measurable_sets m,v) ∧ v ≪ m ⇒
       ∃f. f ∈ Borel_measurable (measurable_space m) ∧ (∀x. 0 ≤ f x) ∧
           ∀s. s ∈ measurable_sets m ⇒ (f * m) s = v s

density_measure_def
  ⊢ ∀m f. f * m = (λs. ∫⁺ m (λx. f x * 𝟙 s x))

Given two such measures $m$ and $v$, The function $f$ asserted by RN theorem is called Radon-Nikodým derivative (denoted by $\displaystyle\frac{\mathrm{d}v}{\mathrm{d}m}$ in textbooks but overloaded on division in HOL):

RN_deriv_def
  ⊢ ∀v m.
       v / m =
       @f. f ∈ Borel_measurable (measurable_space m) ∧
           (∀x. x ∈ m_space m ⇒ 0 ≤ f x) ∧
           ∀s. s ∈ measurable_sets m ⇒ (f * m) s = v s

The overloading of * (density_measure) and / (RN_deriv) with the particular argument order is to syntactically have f * m / m = v / m = f.

Product measures and Fubini's theorem

HOL provides some basic support for (binary) products of measure spaces (in martingale theory).

Let $(X,\mathcal{A},\mu)$ and $(Y,\mathcal{B},\nu)$ be two $\sigma$-finite measure spaces, sometimes we want to define a measure $\rho$ on rectangles of the form $A\times B$ such that $\rho(A\times B) = \mu(A)\nu(B)$ for $A\in\mathcal{A}$ and $B\in\mathcal{B}$. The first problem which we encounter is that the family $\mathcal{A}\times\mathcal{B}$, denoted by prod_sets A B (in sigma_algebra theory):

prod_sets_def
  ⊢ ∀a b. prod_sets a b = {s × t | s ∈ a ∧ t ∈ b}

is, in general, no $\sigma$-algebra — it contains only “rectangles”.

Definition. Let $(X,\mathcal{A})$ and $(Y,\mathcal{B})$ be two measurable spaces, then the $\sigma$-algebra $\mathcal{A}\otimes\mathcal{B} := \sigma(\mathcal{A}\times\mathcal{B})$ is called a product $\sigma$-algebra, and $(X\times Y,\mathcal{A}\otimes\mathcal{B})$ is the product of measurable spaces: ($\times$ is overloaded for $\otimes$ in HOL.)

sigma_algebraTheory.prod_sigma_def
  ⊢ ∀a b.
       a × b = sigma (space a × space b) (prod_sets (subsets a) (subsets b))

The following lemma is quite useful since it allows us to reduce considerations for $\mathcal{A}\otimes\mathcal{B}$ to generators $\mathcal{F}$ and $\mathcal{G}$ of $\mathcal{A}$ and $\mathcal{B}$:

PROD_SIGMA_OF_GENERATOR
  ⊢ ∀X Y E G.
       subset_class X E ∧ subset_class Y G ∧ has_exhausting_sequence (X,E) ∧
       has_exhausting_sequence (Y,G) ⇒
       (X,E) × (Y,G) = sigma X E × sigma Y G

The uniqueness and existence of product measures are guaranteed by the following theorems:

EXISTENCE_OF_PROD_MEASURE
  ⊢ ∀X Y A B u v m0.
       sigma_finite_measure_space (X,A,u) ∧
       sigma_finite_measure_space (Y,B,v) ∧
       (∀s t. s ∈ A ∧ t ∈ B ⇒ m0 (s × t) = u s * v t) ⇒
       (∀s. s ∈ subsets ((X,A) × (Y,B)) ⇒
            (∀x. x ∈ X ⇒ (λy. 𝟙 s (x,y)) ∈ Borel_measurable (Y,B)) ∧
            (∀y. y ∈ Y ⇒ (λx. 𝟙 s (x,y)) ∈ Borel_measurable (X,A)) ∧
            (λy. ∫⁺ (X,A,u) (λx. 𝟙 s (x,y))) ∈ Borel_measurable (Y,B) ∧
            (λx. ∫⁺ (Y,B,v) (λy. 𝟙 s (x,y))) ∈ Borel_measurable (X,A)) ∧
       ∃m. sigma_finite_measure_space (X × Y,subsets ((X,A) × (Y,B)),m) ∧
           (∀s. s ∈ prod_sets A B ⇒ m s = m0 s) ∧
           ∀s. s ∈ subsets ((X,A) × (Y,B)) ⇒
               m s = ∫⁺ (Y,B,v) (λy. ∫⁺ (X,A,u) (λx. 𝟙 s (x,y))) ∧
               m s = ∫⁺ (X,A,u) (λx. ∫⁺ (Y,B,v) (λy. 𝟙 s (x,y)))

UNIQUENESS_OF_PROD_MEASURE
  ⊢ ∀X Y E G A B u v m m'.
       subset_class X E ∧ subset_class Y G ∧ sigma_finite (X,E,u) ∧
       sigma_finite (Y,G,v) ∧ (∀s t. s ∈ E ∧ t ∈ E ⇒ s ∩ t ∈ E) ∧
       (∀s t. s ∈ G ∧ t ∈ G ⇒ s ∩ t ∈ G) ∧ A = sigma X E ∧ B = sigma Y G ∧
       measure_space (X,subsets A,u) ∧ measure_space (Y,subsets B,v) ∧
       measure_space (X × Y,subsets (A × B),m) ∧
       measure_space (X × Y,subsets (A × B),m') ∧
       (∀s t. s ∈ E ∧ t ∈ G ⇒ m (s × t) = u s * v t) ∧
       (∀s t. s ∈ E ∧ t ∈ G ⇒ m' (s × t) = u s * v t) ⇒
       ∀x. x ∈ subsets (A × B) ⇒ m x = m' x

Definition. Let $(X,\mathcal{A})$ and $(Y,\mathcal{B})$ be two measurable spaces, the unique measure $\rho$ constructed in EXISTENCE_OF_PROD_MEASURE is called the product of the measures $\mu$ and $\nu$, denoted by $\mu\times\nu$. $(X\times Y, \mathcal{A}\otimes\mathcal{B}, \mu\times\nu)$ is called the product measure space:

prod_measure_def
  ⊢ ∀m1 m2. prod_measure m1 m2 = (λs. ∫⁺ m2 (λy. ∫⁺ m1 (λx. 𝟙 s (x,y))))

As an application, the two-dimensional (real-valued) Borel measure space (lborel_2d) is constructed by the following definition and theorem:

lborel_2d_def
  ⊢ sigma_finite_measure_space lborel_2d ∧
     m_space lborel_2d = 𝕌(:real) × 𝕌(:real) ∧
     measurable_sets lborel_2d =
     subsets ((𝕌(:real),subsets borel) × (𝕌(:real),subsets borel)) ∧
     (∀s t.
        s ∈ subsets borel ∧ t ∈ subsets borel ⇒
        measure lborel_2d (s × t) = lambda s * lambda t) ∧
     ∀s. s ∈ measurable_sets lborel_2d ⇒
         (∀x. (λy. 𝟙 s (x,y)) ∈ Borel_measurable borel) ∧
         (∀y. (λx. 𝟙 s (x,y)) ∈ Borel_measurable borel) ∧
         (λy. ∫⁺ lborel (λx. 𝟙 s (x,y))) ∈ Borel_measurable borel ∧
         (λx. ∫⁺ lborel (λy. 𝟙 s (x,y))) ∈ Borel_measurable borel ∧
         measure lborel_2d s = ∫⁺ lborel (λy. ∫⁺ lborel (λx. 𝟙 s (x,y))) ∧
         measure lborel_2d s = ∫⁺ lborel (λx. ∫⁺ lborel (λy. 𝟙 s (x,y)))

lborel_2d_prod_measure
  ⊢ ∀s. s ∈ measurable_sets lborel_2d ⇒
         measure lborel_2d s = measure (lborel × lborel) s

The next step is to see how we can integrate w.r.t. $\mu\times\nu$. The following two results are often stated together as the Fubini or Fubini-Tonelli theorem. Following (Schilling 2017), we prefer to distinguish between them since the first result, TONELLI, says that we can always swap iterated integrals of positive functions (even if we get $+\infty$), whereas FUBINI applies to more general functions but requires the (iterated) integrals to be finite:

TONELLI
  ⊢ ∀X Y A B u v f.
       sigma_finite_measure_space (X,A,u) ∧
       sigma_finite_measure_space (Y,B,v) ∧
       f ∈ Borel_measurable ((X,A) × (Y,B)) ∧ (∀s. s ∈ X × Y ⇒ 0 ≤ f s) ⇒
       (∀y. y ∈ Y ⇒ (λx. f (x,y)) ∈ Borel_measurable (X,A)) ∧
       (∀x. x ∈ X ⇒ (λy. f (x,y)) ∈ Borel_measurable (Y,B)) ∧
       (λx. ∫⁺ (Y,B,v) (λy. f (x,y))) ∈ Borel_measurable (X,A) ∧
       (λy. ∫⁺ (X,A,u) (λx. f (x,y))) ∈ Borel_measurable (Y,B) ∧
       ∫⁺ ((X,A,u) × (Y,B,v)) f = ∫⁺ (Y,B,v) (λy. ∫⁺ (X,A,u) (λx. f (x,y))) ∧
       ∫⁺ ((X,A,u) × (Y,B,v)) f = ∫⁺ (X,A,u) (λx. ∫⁺ (Y,B,v) (λy. f (x,y)))

FUBINI
  ⊢ ∀X Y A B u v f.
       sigma_finite_measure_space (X,A,u) ∧
       sigma_finite_measure_space (Y,B,v) ∧
       f ∈ Borel_measurable ((X,A) × (Y,B)) ∧
       (∫⁺ ((X,A,u) × (Y,B,v)) (abs ∘ f) ≠ +∞ ∨
        ∫⁺ (Y,B,v) (λy. ∫⁺ (X,A,u) (λx. (abs ∘ f) (x,y))) ≠ +∞ ∨
        ∫⁺ (X,A,u) (λx. ∫⁺ (Y,B,v) (λy. (abs ∘ f) (x,y))) ≠ +∞) ⇒
       ∫⁺ ((X,A,u) × (Y,B,v)) (abs ∘ f) ≠ +∞ ∧
       ∫⁺ (Y,B,v) (λy. ∫⁺ (X,A,u) (λx. (abs ∘ f) (x,y))) ≠ +∞ ∧
       ∫⁺ (X,A,u) (λx. ∫⁺ (Y,B,v) (λy. (abs ∘ f) (x,y))) ≠ +∞ ∧
       integrable ((X,A,u) × (Y,B,v)) f ∧
       (AE y::(Y,B,v). integrable (X,A,u) (λx. f (x,y))) ∧
       (AE x::(X,A,u). integrable (Y,B,v) (λy. f (x,y))) ∧
       integrable (X,A,u) (λx. ∫ (Y,B,v) (λy. f (x,y))) ∧
       integrable (Y,B,v) (λy. ∫ (X,A,u) (λx. f (x,y))) ∧
       ∫ ((X,A,u) × (Y,B,v)) f = ∫ (Y,B,v) (λy. ∫ (X,A,u) (λx. f (x,y))) ∧
       ∫ ((X,A,u) × (Y,B,v)) f = ∫ (X,A,u) (λx. ∫ (Y,B,v) (λy. f (x,y)))

HOL also supports other types of “products”. In general, any three LISP-like pair operators can be used for building product measures:

pair_operation_def
  ⊢ ∀cons car cdr.
       pair_operation cons car cdr ⇔
       (∀a b. car (cons a b) = a ∧ cdr (cons a b) = b) ∧
       ∀a b c d. cons a b = cons c d ⇔ a = c ∧ b = d

pair_operation_pair
  ⊢ pair_operation $, FST SND

Besides pairTheory, one can also use FCP_CONCAT, FCP_FST and FCP_SND (fcpTheory):

pair_operation_FCP_CONCAT
  ⊢ FINITE 𝕌(:β) ∧ FINITE 𝕌(:γ) ⇒ pair_operation FCP_CONCAT FCP_FST FCP_SND

The general version of product space is defined on any pair operation:

general_cross_def
  ⊢ ∀cons A B. general_cross cons A B = {cons a b | a ∈ A ∧ b ∈ B}

general_prod_def
  ⊢ ∀cons A B.
       general_prod cons A B = {general_cross cons a b | a ∈ A ∧ b ∈ B}

In fact, all the theorems about product measures, up to the uniqueness and existence of product measures (i.e. except for TONELLI and FUBINI), are first proven on general pair operations (and then specialized on pairTheory and fcpTheory). For instance, the following theorem is the general version of EXISTENCE_OF_PROD_MEASURE:

existence_of_prod_measure_general
  ⊢ ∀cons car cdr X Y A B u v m0.
       pair_operation cons car cdr ∧ sigma_finite_measure_space (X,A,u) ∧
       sigma_finite_measure_space (Y,B,v) ∧
       (∀s t. s ∈ A ∧ t ∈ B ⇒ m0 (general_cross cons s t) = u s * v t) ⇒
       (∀s. s ∈ subsets (general_sigma cons (X,A) (Y,B)) ⇒
            (∀x. x ∈ X ⇒ (λy. 𝟙 s (cons x y)) ∈ Borel_measurable (Y,B)) ∧
            (∀y. y ∈ Y ⇒ (λx. 𝟙 s (cons x y)) ∈ Borel_measurable (X,A)) ∧
            (λy. ∫⁺ (X,A,u) (λx. 𝟙 s (cons x y))) ∈ Borel_measurable (Y,B) ∧
            (λx. ∫⁺ (Y,B,v) (λy. 𝟙 s (cons x y))) ∈ Borel_measurable (X,A)) ∧
       ∃m. sigma_finite_measure_space
             (general_cross cons X Y,
              subsets (general_sigma cons (X,A) (Y,B)),m) ∧
           (∀s. s ∈ general_prod cons A B ⇒ m s = m0 s) ∧
           ∀s. s ∈ subsets (general_sigma cons (X,A) (Y,B)) ⇒
               m s = ∫⁺ (Y,B,v) (λy. ∫⁺ (X,A,u) (λx. 𝟙 s (cons x y))) ∧
               m s = ∫⁺ (X,A,u) (λx. ∫⁺ (Y,B,v) (λy. 𝟙 s (cons x y)))

$\mathcal{L}^p$ spaces, semi-norm and important inequalities

HOL provides a minimal formalization of the function spaces $\mathcal{L}^p$, currently in martingaleTheory.

Let $(X, \mathscr{A}, \mu)$ be some measure space. Here we discuss functions whose (absolute) $p$th power is integrable ($1\leqslant p <\infty$). More precisely, we are interested in the sets:

$$\mathcal{L}^p(\mu) := \left\{ u \colon X\rightarrow\overline{\mathbb{R}} \colon u\in\mathcal{M}(\mathscr{A}), \int |u|^p \,\mathrm{d}\mu < \infty \right\}, \qquad p\in[1,\infty).$$

Or formally:

lp_space_alt_finite
  ⊢ ∀p m f.
       0 < p ∧ p ≠ +∞ ⇒
       (f ∈ lp_space p m ⇔
        f ∈ Borel_measurable (measurable_space m) ∧
        ∫⁺ m (λx. abs (f x) powr p) ≠ +∞)

It is convenient to have the following semi-norm notation:

$$\|u\|_p := \left( \int \left|u(x)\right|^p \mu(\mathrm{d}x) \right)^{1/p}, \qquad p\in[1,\infty).$$

Or formally:

seminorm_normal
  ⊢ ∀p m f.
       0 < p ∧ p ≠ +∞ ⇒ seminorm p m f = ∫⁺ m (λx. abs (f x) powr p) powr p⁻¹

Clearly, $u\in\mathcal{L}^p(\mu)$ if, and only if, $u\in\mathcal{M}(\mathscr{A})$ and $\|u\|_p <\infty$. Furthermore, $\|u\|_p$ implies that $u(x) = 0$ for almost every $x$.

lp_space_alt_seminorm
  ⊢ ∀p m f.
       measure_space m ∧ 0 < p ⇒
       (f ∈ lp_space p m ⇔
        f ∈ Borel_measurable (measurable_space m) ∧ seminorm p m f < +∞)

seminorm_eq_0
  ⊢ ∀p m f.
       measure_space m ∧ 0 < p ∧ f ∈ Borel_measurable (measurable_space m) ⇒
       (seminorm p m f = 0 ⇔ AE x::m. f x = 0)

The triangle inequality for $\|\cdot\|_p$ and deeper results on $\mathcal{L}^p$ depends on the following elementary inequality (Young's inequality):

extrealTheory.young_inequality
  ⊢ ∀a b p q.
       0 ≤ a ∧ 0 ≤ b ∧ 0 < p ∧ 0 < q ∧ p ≠ +∞ ∧ q ≠ +∞ ∧ p⁻¹ + q⁻¹ = 1 ⇒
       a * b ≤ a powr p / p + b powr q / q

Then the following fundamental Hölder's inequality is proved:

Hoelder_inequality
  ⊢ ∀m u v p q.
       measure_space m ∧ 0 < p ∧ 0 < q ∧ p⁻¹ + q⁻¹ = 1 ∧ u ∈ lp_space p m ∧
       v ∈ lp_space q m ⇒
       integrable m (λx. u x * v x) ∧
       ∫ m (λx. abs (u x * v x)) ≤ seminorm p m u * seminorm q m v

Holder's inequality with $p = q = 2$ is usually called the Cauchy-Schwarz inequality:

Cauchy_Schwarz_inequality
  ⊢ ∀m u v.
       measure_space m ∧ u ∈ L2_space m ∧ v ∈ L2_space m ⇒
       integrable m (λx. u x * v x) ∧
       ∫ m (λx. abs (u x * v x)) ≤ seminorm 2 m u * seminorm 2 m v

Another consequence of Holder's inequality is the Minkowski or triangle inequality for $\|\cdot\|_p$:

Minkowski_inequality
  ⊢ ∀p m u v.
       measure_space m ∧ 1 ≤ p ∧ u ∈ lp_space p m ∧ v ∈ lp_space p m ⇒
       (λx. u x + v x) ∈ lp_space p m ∧
       seminorm p m (λx. u x + v x) ≤ seminorm p m u + seminorm p m v

Holder's inequality is useful in Probability Theory, for example, for proving that the sum of random variables having finite second moments (i.e. finite variance), has still finite second moments.

Equivalence of Lebesuge and Henstock-Kurzweil (Gauge) integration

Concrete Lebesgue integrals in the lborel measure space may be calculated by Fundamental Theorem of Calculus (FTC) of Henstock-Kurzweil (Gauge) integrals. Note that, a (Borel-measurable) function is Lebesgue integrable iff it is Gauge absolutely integrable. In lebesgue_measure theory, the following theorems are proved to show the equivalence of the two integration systems:

lebesgue_eq_gauge_integral
  ⊢ ∀f. integrable lborel (Normal ∘ f) ⇒
         integration$absolutely_integrable_on f 𝕌(:real) ∧
         ∫ lborel (Normal ∘ f) = Normal (integration$integral 𝕌(:real) f)

lebesgue_eq_gauge_integral_alt
  ⊢ ∀f. f ∈ borel_measurable borel ∧
         integration$absolutely_integrable_on f 𝕌(:real) ⇒
         integrable lborel (Normal ∘ f) ∧
         ∫ lborel (Normal ∘ f) = Normal (integration$integral 𝕌(:real) f)

lebesgue_eq_gauge_integrable
  ⊢ ∀f. f ∈ borel_measurable borel ⇒
         (integrable lborel (Normal ∘ f) ⇔
          integration$absolutely_integrable_on f 𝕌(:real))

Note that various forms of FTCs in integration theory can only compute the integration of a function over a closed or open interval (excluding at most a countable set of points, see FUNDAMENTAL_THEOREM_OF_CALCULUS_STRONG). On the other hand, Lebesgue integration always integrates a function over the entire measure space, and integrating over an interval is equivalent to integrating over the entire measure space the same function multiplied with an indicator function whose indicating set is the interval. The basic form of a FTC-like theorem for Lebesgue integration, based on FUNDAMENTAL_THEOREM_OF_CALCULUS, is the following one:

FTC_integral_lborel
  ⊢ ∀f f' g a b.
       a ≤ b ∧
       (∀x. x ∈ interval [(a,b)] ⇒
            (f has_vector_derivative f' x) (at x within interval [(a,b)])) ∧
       f' ∈ borel_measurable borel ∧
       (∀x. g x = f' x * indicator (interval [(a,b)]) x) ∧
       (integrable lborel (Normal ∘ g) ∨
        integration$absolutely_integrable_on g 𝕌(:real)) ⇒
       ∫ lborel (Normal ∘ g) = Normal (f b − f a)

To satisfy the integrability antecedents in the above theorem, one may need to find a bound function w such that !x. abs (g x) <= w x, which is known to be integrable (in Lebesuge integration) or absolutely integrable (in Gauge integration).

To compute improper integrals, i.e. integrals over half intervals or the entire reals, one can use monotone or Lebesgue dominated convergence theorems to obtain the improper integral as the limits of interval integrals. (See examples/probability/distribution for some examples of improper integrals related to normal distributions.)

Probability Theory

HOL provides a comprehensive formalization of probability theory based on the thesis work of Joe Hurd (Hurd 2002) (2002), Aaron R. Coble (Coble 2010) (2010), Osman Hasan (Hasan 2008) (2008), Tarek Mhamdi (Mhamdi 2012) (2012) and Muhammad Qasim (Qasim 2016) (2016). In HOL, the probability theory depends on pred_set, measure, borel and lebesgue theories, etc.

Probability space

Probability theory enables us to give a probabilistic-statistical description of experiments with a finite or infinite number of outcomes. Based on Kolmogorov's axiomatization (Kolmogorov 1950), a probabilistic model (of an experiment) or a probability space (prob_space) is an ordered triple $(\Omega,\mathscr{F},\mathscr{P})$ where,

  • $\Omega$ is a set of points $\omega$, also called a sample space (of “elementary events” or outcomes);
  • $\mathscr{F}$ is a $\sigma$-algebra of subsets of $\Omega$;
  • $\mathscr{P}$ is a probability measure on $\mathscr{F}$ ($\mathscr{P}\{\cdot\} \in [0,1]$).

Let p be a probability space, i.e. prob_space p, the functions p_space, events and prob can be used to access each components of the triple. Thus a probability space is nothing but a measure space where the measure of the sample space is one:

prob_space_def
  ⊢ ∀p. prob_space p ⇔ measure_space p ∧ measure p (m_space p) = 1
p_space_def
  ⊢ p_space = m_space
events_def
  ⊢ events = measurable_sets
prob_def
  ⊢ prob = measure

If E is an event, the statement that “the probability of $E$ is $r$” ($\mathscr{P}\{E\} = r$) is denoted by prob p e = r in HOL. As probability theory is based on set theory, it is useful to have a table (see Table 6.6.1) displaying the ways in which various concepts are interpreted in the two theories.

Table: Various concepts in set theory and probability theory.

NotationSet-theoretic interpretationInterpretation in probability theory
$\omega$Element or pointOutcome, sample point, elementary event
$\Omega$Set of pointsSample space; certain event
$\mathscr{F}$$\sigma$-algebra of subsets$\sigma$-algebra of events
$A \in \mathscr{F}$Set of pointsEvent (if $\omega\in A$, we say that event $A$ occurs)
$\overline{A}\in\Omega\setminus A$Complement of $A$, i.e. the set of points $\omega$ that are not in $A$Event that $A$ does not occur
$A\cup B$Union of $A$ and $B$, i.e. the set of points $\omega$ belonging either to $A$ or to $B$ (or to both)Event that either $A$ or $B$ (or both) occurs
$A\cap B$Intersection of $A$ and $B$, i.e. the set of points $\omega$ belonging to both $A$ and $B$Event that both $A$ and $B$ occur
$\emptyset$Empty setImpossible event
$A\cap B =\emptyset$$A$ and $B$ are disjointEvents $A$ and $B$ are mutually exclusive, i.e. cannot occur simultaneously
$A\setminus B$Difference of $A$ and $B$, i.e. the set of points that belong to $A$ but not to $B$Event that $A$ occurs and $B$ does not

Many properties of probability and events are migrated directly from measure theory. For instance, the following theorems states that probability is increasing, additive and countably additive, respectively:

PROB_INCREASING
  ⊢ ∀p s t.
       prob_space p ∧ s ∈ events p ∧ t ∈ events p ∧ s ⊆ t ⇒
       prob p s ≤ prob p t

PROB_ADDITIVE
  ⊢ ∀p s t u.
       prob_space p ∧ s ∈ events p ∧ t ∈ events p ∧ DISJOINT s t ∧ u = s ∪ t ⇒
       prob p u = prob p s + prob p t

PROB_COUNTABLY_ADDITIVE
  ⊢ ∀p s f.
       prob_space p ∧ f ∈ (𝕌(:num) → events p) ∧
       (∀m n. m ≠ n ⇒ DISJOINT (f m) (f n)) ∧ s = BIGUNION (IMAGE f 𝕌(:num)) ⇒
       prob p s = suminf (prob p ∘ f)

Also note that, the type of probabilities is $\konst{extreal}$ in HOL10, even the actual probability values are always between 0 and 1. The following simple theorem which states that the probability of any event cannot be infinite, is useful in many proofs (when dealing with extreal arithmetics):

PROB_FINITE
  ⊢ ∀p s. prob_space p ∧ s ∈ events p ⇒ prob p s ≠ −∞ ∧ prob p s ≠ +∞

Conditional probability

HOL provides a formalization of elementary “conditional probability” (Shiryaev 2016, 95:21), originally developed by Liya Liu in 2012, in both probability and real_probability theories. The probability that $e_1$ occurs assuming $e_2$ occurs, i.e. $\mathscr{P}\{e_1 \mid e_2\}$, is denoted by cond_prob p e1 e2 in HOL:

cond_prob_def
  ⊢ ∀p e1 e2. cond_prob p e1 e2 = prob p (e1 ∩ e2) / prob p e2

Among other common properties, the Total Probability Formula and the famous Bayes' Rule (specific and general versions) (Feller 2004, vol. 1, chap. V) are provided:

TOTAL_PROB_SIGMA
  ⊢ ∀p A B s.
       prob_space p ∧ A ∈ events p ∧ FINITE s ∧
       (∀x. x ∈ s ⇒ B x ∈ events p ∧ prob p (B x) ≠ 0) ∧
       (∀a b. a ∈ s ∧ b ∈ s ∧ a ≠ b ⇒ DISJOINT (B a) (B b)) ∧
       BIGUNION (IMAGE B s) = p_space p ⇒
       prob p A = ∑ (λi. prob p (B i) * cond_prob p A (B i)) s

BAYES_RULE
  ⊢ ∀p A B.
       prob_space p ∧ A ∈ events p ∧ B ∈ events p ∧ prob p A ≠ 0 ⇒
       cond_prob p B A = cond_prob p A B * prob p B / prob p A

BAYES_RULE_GENERAL_SIGMA
  ⊢ ∀p A B s k.
       prob_space p ∧ A ∈ events p ∧ prob p A ≠ 0 ∧ FINITE s ∧
       (∀x. x ∈ s ⇒ B x ∈ events p ∧ prob p (B x) ≠ 0) ∧ k ∈ s ∧
       (∀a b. a ∈ s ∧ b ∈ s ∧ a ≠ b ⇒ DISJOINT (B a) (B b)) ∧
       BIGUNION (IMAGE B s) = p_space p ⇒
       cond_prob p (B k) A =
       cond_prob p A (B k) * prob p (B k) /
       ∑ (λi. prob p (B i) * cond_prob p A (B i)) s

Random variable

The concept “random variable” (r.v.) serves to define quantities describing the results of “measurements” in random experiments. A random variable is a measurable function from a probability space $p$ to a $\sigma$-algebra (measurable space) $s$, denoted by random_variable X p s in HOL. However, many theorems in probability focus on extreal- and finite-valued r.v.'s from probability spaces to Borel sets. They are called real_random_variable:

random_variable_def
  ⊢ ∀X p s. random_variable X p s ⇔ X ∈ measurable (p_space p,events p) s

real_random_variable_def
  ⊢ ∀X p.
       real_random_variable X p ⇔
       random_variable X p Borel ∧ ∀x. x ∈ p_space p ⇒ X x ≠ −∞ ∧ X x ≠ +∞

Let the probability space be $(\Omega,\mathscr{F},\mathscr{P})$, a (probability) distribution is a probability measure on $(\Omega,\mathscr{F})$, while a distribution function is a special distribution defined on half spaces $(-\infty,x]$:

distribution_def
  ⊢ ∀p X. distribution p X = (λs. prob p (PREIMAGE X s ∩ p_space p))

distribution_function
  ⊢ ∀p X t. distribution_function p X t = distribution p X {x | x ≤ t}

Note that the distribution (measure) of any random variable also forms a probability space with its target measurable space:

distribution_prob_space
  ⊢ ∀p X s.
       prob_space p ∧ sigma_algebra s ∧ random_variable X p s ⇒
       prob_space (space s,subsets s,distribution p X)

Independence

The concept of mutual independence of two or more experiments holds, in a certain sense, a central position in the theory of probability. — Kolmogorov (Kolmogorov 1950, 8)

Informally, events or random variables are independent if they do not affect each other's probabilities. Thus, two events $A$ and $B$ are independent if $\mathscr{P}\{A \cap B\} = \mathscr{P}\{A\}\mathscr{P}\{B\}$. Moreover, a finite collection of events is said to be pairwise independent if each two of them are independent:

indep_def
  ⊢ ∀p a b.
       indep p a b ⇔
       a ∈ events p ∧ b ∈ events p ∧ prob p (a ∩ b) = prob p a * prob p b

pairwise_indep_events_def
  ⊢ ∀p E J.
       pairwise_indep_events p E J ⇔
       ∀i j. i ∈ J ∧ j ∈ J ∧ i ≠ j ⇒ indep p (E i) (E j)

More generally, a possibly infinite sequence of events $\{E_\alpha\}_{\alpha\in J}$ is said to be (totally) independent if for each $j\in\mathbb{N}$ and distinct finite choice $\alpha_1,\alpha_2,\ldots,\alpha_j \in J$, we have

$$\mathscr{P}\{E_{\alpha_1} \cap E_{\alpha_2} \cap \cdots \cap E_{\alpha_j}\} = \mathscr{P}\{E_{\alpha_1}\} \,\mathscr{P}\{E_{\alpha_2}\} \,\cdots\, \mathscr{P}\{E_{\alpha_j}\}$$

or formally

indep_events_def
  ⊢ ∀p E J.
       indep_events p E J ⇔
       ∀N. N ⊆ J ∧ N ≠ ∅ ∧ FINITE N ⇒
           prob p (BIGINTER (IMAGE E N)) = ∏ (prob p ∘ E) N

We shall on occasion also talk about independence of collections (sets) of events:

indep_families_def
  ⊢ ∀p q r. indep_sets p q r ⇔ ∀s t. s ∈ q ∧ t ∈ r ⇒ indep p s t

We shall also talk about independence of r.v.'s. Random variables $X$ and $Y$ are independent if for all Borel sets $a$ and $b$, the events $X^{-1}(a)$ and $Y^{-1}(b)$ are independent:

indep_rv_def
  ⊢ ∀p X Y s t.
       indep_vars p X Y s t ⇔
       ∀a b.
         a ∈ subsets s ∧ b ∈ subsets t ⇒
         indep p (PREIMAGE X a ∩ p_space p) (PREIMAGE Y b ∩ p_space p)

For sequences of events and r.v.'s there are also pairwise (pairwise_indep_sets and pairwise_indep_vars) and total (indep_sets, indep_vars) independences with similar definitions as pairwise_indep_events and indep_events, respectively. Note that total independence always implies pairwise independence:

total_imp_pairwise_indep_events
  ⊢ ∀p E J.
       (∀n. n ∈ J ⇒ E n ∈ events p) ∧ indep_events p E J ⇒
       pairwise_indep_events p E J

total_imp_pairwise_indep_sets
  ⊢ ∀p A J.
       (∀n. n ∈ J ⇒ A n ⊆ events p) ∧ indep_sets p A J ⇒
       pairwise_indep_sets p A J

total_imp_pairwise_indep_vars
  ⊢ ∀p X A J.
       prob_space p ∧ (∀i. i ∈ J ⇒ random_variable (X i) p (A i)) ∧
       (∀i. i ∈ J ⇒ sigma_algebra (A i)) ∧ indep_vars p X A J ⇒
       pairwise_indep_vars p X A J

Limit events

An important concept in set theory is that of the "$\limsup$" and "$\liminf$" of a sequence of sets. These notions can be defined for subsets of an arbitrary sample space. Let $E_n$ be any sequence of events, we define the following two “limit events”:

$$\limsup_n E_n = \bigcap_{m=1}^{\infty} \bigcup_{n=m}^{\infty} E_n, \qquad \liminf_n E_n = \bigcup_{m=1}^{\infty} \bigcap_{n=m}^{\infty} E_n.$$

or formally

sigma_algebraTheory.set_limsup_def
  ⊢ ∀E. limsup E = BIGINTER (IMAGE (λm. BIGUNION {E n | m ≤ n}) 𝕌(:num))

sigma_algebraTheory.set_liminf_def
  ⊢ ∀E. liminf E = BIGUNION (IMAGE (λm. BIGINTER {E n | m ≤ n}) 𝕌(:num))

EVENTS_LIMSUP
  ⊢ ∀p E. prob_space p ∧ (∀n. E n ∈ events p) ⇒ limsup E ∈ events p

EVENTS_LIMINF
  ⊢ ∀p E. prob_space p ∧ (∀n. E n ∈ events p) ⇒ liminf E ∈ events p

Intuitively, a point belongs to $\limsup_n E_n$ if and only if it belongs to infinitely many terms of the sequence $\{E_n\}$. A point belongs to $\liminf_n E_n$ if and only if it belongs to all terms of the sequence from a certain term on (i.e. almost always). The following two deep results, together referred to as the Borel-Cantelli lemma (Chung 2001, 80), are useful in proving other deep results in probability theory:

Borel_Cantelli_Lemma1
  ⊢ ∀p E.
       prob_space p ∧ (∀n. E n ∈ events p) ∧ suminf (prob p ∘ E) < +∞ ⇒
       prob p (limsup E) = 0

Borel_Cantelli_Lemma2p
  ⊢ ∀p E.
       prob_space p ∧ (∀n. E n ∈ events p) ∧
       pairwise_indep_events p E 𝕌(:num) ∧ suminf (prob p ∘ E) = +∞ ⇒
       prob p (limsup E) = 1

Tail algebra and tail events

Given a sequence of events $E_1, E_2, \ldots$, we define their tail algebra (or tail field) by

$$\tau = \bigcap_{n=1}^\infty \sigma(E_n, E_{n+1}, E_{n+2}, \ldots)$$

or formally

tail_algebra_def
  ⊢ ∀p E.
       tail_algebra p E =
       (p_space p,
        BIGINTER
          (IMAGE (λn. subsets (sigma (p_space p) (IMAGE E (from n)))) 𝕌(:num)))

In words, an event $E \in \tau$ must have the property that for any $n$, it depends only on the events $E_n, E_{n+1}, \ldots$; in particular, it does not care about any finite number of the events $E_n$. Events that belongs to tail algebras is called tail events (or remote events). A surprising theorem is the following Kolmogorov Zero-One Law (Rosenthal 2006, 37) which states that, if the tail algebra is generated from independent events, the probability of remote events can only be zero or one:

Kolmogorov_0_1_Law
  ⊢ ∀p E.
       prob_space p ∧ (∀n. E n ∈ events p) ∧ indep_events p E 𝕌(:num) ⇒
       ∀e. e ∈ subsets (tail_algebra p E) ⇒ prob p e = 0 ∨ prob p e = 1

Mathematical expectation

To achieve reasonable simplicity it is often necessary to describe probability distributions rather summarily by a few 'typical values.' ... Among the typical values the (mathematical) expectation, or mean, is by far the most important. It lends itself best to analytical manipulations, and it is preferred by statisticians because of a property known as sampling stability. — Feller (Feller 2004, 1:221)

Formally, expectation is nothing but a synonym of (Lebesgue) integration:

expectation_def
  ⊢ expectation = ∫

Thus most properties of expectation in probability theory are migrated from properties of Lebesgue integration. For instance, the following theorem is the probability version of the Markov's inequality:

prob_markov_ineq
  ⊢ ∀p X c.
       prob_space p ∧ integrable p X ∧ 0 < c ⇒
       prob p ({x | c ≤ abs (X x)} ∩ p_space p) ≤
       c⁻¹ * expectation p (abs ∘ X)

In particular, for any random variable $X$ the value expectation p X (usually denoted by $\mathscr{E}(X)$ in textbooks) exists if and only if the random variable as a measurable function is integrable, i.e. integrable p X.

Second moments and variance

Let $a$ be extreal, $r$ positive, then $\mathscr{E}(|X - a|^r)$ is called the absolute moment of $X$ of order $r$, about $a$. It may be $+\infty$; otherwise, and if $r$ is an integer, $\mathscr{E}((X - a)^r)$ is the corresponding moment:

absolute_moment_def
  ⊢ ∀p X a r.
       absolute_moment p X a r = expectation p (λx. abs (X x − a) pow r)

moment_def
  ⊢ ∀p X a r. moment p X a r = expectation p (λx. (X x − a) pow r)

For $r = 1$, $a = 0$, this reduces to $\mathscr{E}(X)$ (expectation or mean). The moments about the mean are called central moments. That of order 2 is particularly important and is called the variance, usually denoted by $\sigma^2(X)$ or $\mathrm{var}(X)$:

central_moment_def
  ⊢ ∀p X r. central_moment p X r = moment p X (expectation p X) r

variance_def
  ⊢ ∀p X. variance p X = central_moment p X 2

variance_alt
  ⊢ ∀p X. variance p X = expectation p (λx. (X x − expectation p X)²)

The formula $\sigma^2(X) = \mathscr{E}(X^2) - \mathscr{E}(X)^2$ is well known in elementary probability, so is Chebyshev's inequality:

variance_eq
  ⊢ ∀p X.
       prob_space p ∧ real_random_variable X p ∧ integrable p (λx. (X x)²) ⇒
       variance p X = expectation p (λx. (X x)²) − (expectation p X)²

chebyshev_ineq_variance
  ⊢ ∀p X t.
       prob_space p ∧ real_random_variable X p ∧ finite_second_moments p X ∧
       0 < t ⇒
       prob p ({x | t ≤ abs (X x − expectation p X)} ∩ p_space p) ≤
       (t²)⁻¹ * variance p X

Note that, for the above formulae to hold, the r.v. $X$ must have finite second moments, which has several equivalent definitions:

finite_second_moments_def
  ⊢ ∀p X. finite_second_moments p X ⇔ ∃a. second_moment p X a < +∞

finite_second_moments_literally
  ⊢ ∀p X.
       prob_space p ∧ real_random_variable X p ⇒
       (finite_second_moments p X ⇔ expectation p (λx. (X x)²) < +∞)

finite_second_moments_eq_integrable_square
  ⊢ ∀p X.
       prob_space p ∧ real_random_variable X p ⇒
       (finite_second_moments p X ⇔ integrable p (λx. (X x)²))

finite_second_moments_eq_finite_variance
  ⊢ ∀p X.
       prob_space p ∧ real_random_variable X p ⇒
       (finite_second_moments p X ⇔ variance p X < +∞)

In particular, if a (finite-valued) random variable $X$ has finite second moments, or equivalently $X^2$ is integrable, then $X$ itself is also integrable:

finite_second_moments_imp_integrable
  ⊢ ∀p X.
       prob_space p ∧ real_random_variable X p ∧ finite_second_moments p X ⇒
       integrable p X

Finally, for uncorrelated r.v.'s (i.e. $\mathscr{E}(XY) = \mathscr{E}(X)\mathscr{E}(Y)$), the following “additivity of the variance” holds:

uncorrelated_def
  ⊢ ∀p X Y.
       uncorrelated p X Y ⇔
       finite_second_moments p X ∧ finite_second_moments p Y ∧
       expectation p (λs. X s * Y s) = expectation p X * expectation p Y
uncorrelated_vars_def
  ⊢ ∀p X J.
       uncorrelated_vars p X J ⇔
       ∀i j. i ∈ J ∧ j ∈ J ∧ i ≠ j ⇒ uncorrelated p (X i) (X j)

variance_sum
  ⊢ ∀p X J.
       prob_space p ∧ FINITE J ∧ (∀i. i ∈ J ⇒ real_random_variable (X i) p) ∧
       uncorrelated_vars p X J ⇒
       variance p (λx. ∑ (λn. X n x) J) = ∑ (λn. variance p (X n)) J

Convergence of random sequences

Given a (countable) sequence of finite r.v.'s $X_n$ and another finite r.v. $Y$, the following three concepts of convergence (from $X_n$ to $Y$) are defined in HOL (assuming all involved r.v.'s satisfy real_random_variable):

  1. The sequence of r.v. $\{X_n\}$ is said to converge almost everywhere (a.e.) (to the r.v. $Y$) iff there exists a null set $\mathbf{N}$ such that

    $$\forall \omega\in\Omega\setminus\mathbf{N}.\, \lim_{n\rightarrow\infty} X_n(\omega) = Y(\omega)\;\text{finite.}$$

    or formally

    probabilityTheory.converge_AE_def
      ⊢ ∀p X Y.
           (∀n. real_random_variable (X n) p) ∧ real_random_variable Y p ⇒
           ((X ⟶ Y) (almost_everywhere p) ⇔
            AE x::p. ((λn. real (X n x)) ⟶ real (Y x)) sequentially)
    
  2. The sequence $\{X_n\}$ is said to converge in probability (pr.) to $Y$ iff for every $\epsilon > 0$ we have

    $$\lim_{n\rightarrow\infty} \mathscr{P}\{|X_n - Y| > \epsilon \} = 0.$$

    or formally

    probabilityTheory.converge_PR_def
      ⊢ ∀p X Y.
           prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
           real_random_variable Y p ⇒
           ((X ⟶ Y) (in_probability p) ⇔
            ∀e. 0 < e ∧ e ≠ +∞ ⇒
                ((λn. real (prob p {x | x ∈ p_space p ∧ e < abs (X n x − Y x)})) ⟶
                 0) sequentially)
    
  3. The sequence $\{X_n\}$ is said to converge in $L^p$ to $Y$ iff $X_n \in L^p$, $Y \in L^p$ and

    $$\lim_{n\rightarrow\infty} \mathscr{E}(|X_n - Y|^p) = 0.$$

    or formally

    probabilityTheory.converge_LP_def
      ⊢ ∀p X Y r.
           prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
           real_random_variable Y p ∧ 0 < r ∧ r ≠ +∞ ⇒
           ((X ⟶ Y) (in_lebesgue r p) ⇔
            (∀n. X n ∈ lp_space r p) ∧ Y ∈ lp_space r p ∧
            ((λn. real (expectation p (λx. abs (X n x − Y x) powr r))) ⟶ 0)
              sequentially)
    

The above three definitions all look very different with each other. And it is actually very hard to work directly with converge_AE, which says that, except for a null set, for all other point $\omega$ in the sample space, the limit of $X_n(\omega)$ is $Y(\omega)$. However, several equivalent theorems can be used to convert that “limit” to something else, namely $\sup$, $\inf$, $\limsup$ and $\liminf$. The most important one is the following:

converge_AE_alt_limsup
  ⊢ ∀p X Y.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       real_random_variable Y p ⇒
       ((X ⟶ Y) (almost_everywhere p) ⇔
        ∀e. 0 < e ∧ e ≠ +∞ ⇒
            prob p (limsup (λn. {x | x ∈ p_space p ∧ e < abs (X n x − Y x)})) =
            0)

Recall that $\limsup_n E_n$ means “$E_n$ occurs infinitely often” (cf. Borel-Cantelli Lemma). Now it should be clear why “convergence a.e.” implies “convergence in pr.”: the former says that it is impossible (zero probability) that $|X_n(\omega) - Y(\omega)| > e$ holds “infinitely often” when $n$ increases. In another words, after certain $N$, for all $n > N$, $\mathscr{P}(|X_n(\omega) - Y(\omega)| > e)$ is exactly zero. Of course this implies “in pr.” which only requires this probability be “close to zero”. The converse does not hold, simply because a probability very small and close to zero is still not zero, thus it is not a null set, and cannot fit with the definition of “convergence a.e.”. Indeed, we can prove that “a.e.” implies “in pr.”:

converge_AE_imp_PR
  ⊢ ∀p X Y.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       real_random_variable Y p ∧ (X ⟶ Y) (almost_everywhere p) ⇒
       (X ⟶ Y) (in_probability p)

converge_AE_imp_PR'
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       (X ⟶ (λx. 0)) (almost_everywhere p) ⇒
       (X ⟶ (λx. 0)) (in_probability p)

Note that in all above concepts the convergence “from $X$ to $Y$” and “from $X - Y$ to 0” are equivalent, for instance:

converge_AE_to_zero
  ⊢ ∀p X Y.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       real_random_variable Y p ⇒
       ((X ⟶ Y) (almost_everywhere p) ⇔
        ((λn x. X n x − Y x) ⟶ (λx. 0)) (almost_everywhere p))

converge_PR_to_zero
  ⊢ ∀p X Y.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       real_random_variable Y p ⇒
       ((X ⟶ Y) (in_probability p) ⇔
        ((λn x. X n x − Y x) ⟶ (λx. 0)) (in_probability p))

On the other hand, $L^p$ convergence implies convergence in probability, but so far only a special version (converge to zero, with integer-valued power) is provided:

converge_LP_imp_PR'
  ⊢ ∀p X k.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧ 0 < k ∧
       (X ⟶ (λx. 0)) (in_lebesgue (&k) p) ⇒
       (X ⟶ (λx. 0)) (in_probability p)

Limit theorems and the Laws of Large Numbers

In the formal construction of a course in the theory of probability, limit theorems appear as a kind of superstructure over elementary chapters, in which all problems have finite, purely arithmetical character. In reality, however, the epistemological value of the theory of probability is revealed only by limit theorems. Moreover, without limit theorems it is impossible to understand the real content of the primary concept of all our sciences — the concept of probability. — Gnedenko (Gnedenko and Kolmogorov 1954, 1)

It is sometimes convenient to think of probabilities intuitively as limits of observable frequencies in repeated experiments. This would lead to the following intuitive interpretation of the expectation. Let an experiment be repeated $n$ times 'under identical conditions,' and denote by $X_1,\ldots, X_n$ the values of $X$ that were actually observed. For large $n$ the average $(X_1 + \cdots + X_n)/n$ should be close to $\mathscr{E}(X)$. The laws of large numbers give substance and precision to this vague intuitive description (Feller 2004, 1:221).

More generally, the so-called “Law of Large Numbers” (LLN) deals with the partial sums of an infinite sequence of random variables $S_n = \sum_{j=1}^n X_j$. Under certain conditions $(S_n - \mathscr{E}(S_n))/n$ converges to 0, in probability (weak law) or almost everywhere (strong law).

Several versions of the Laws of Large Numbers can be found in large_number theory, in $(HOLDIR)/examples/probability directory. The conclusion of all LLN theorems are given in the form of LLN, which has alternative definitions for different conditions, e.g.:

LLN_def
  ⊢ ∀p X convergence_mode.
       LLN p X convergence_mode ⇔
       (let
          Z n x = ∑ (λi. X i x) (count1 n)
        in
          ((λn x. (Z n x − expectation p (Z n)) / &SUC n) ⟶ (λx. 0))
            (convergence_mode p))

LLN_alt_converge_AE_IID
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       identical_distribution p X Borel 𝕌(:num) ∧ integrable p (X 0) ⇒
       (LLN p X almost_everywhere ⇔
        ((λn x. ∑ (λi. X i x) (count1 n) / &SUC n) ⟶
         (λx. expectation p (X 0))) (almost_everywhere p))

LLN_alt_converge_PR_IID
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       identical_distribution p X Borel 𝕌(:num) ∧ integrable p (X 0) ⇒
       (LLN p X in_probability ⇔
        ((λn x. ∑ (λi. X i x) (count1 n) / &SUC n) ⟶
         (λx. expectation p (X 0))) (in_probability p))

For uncorrelated r.v.'s with a common bound of variance, the proof is simple and based on Markov's and Chebyshev's inequalities. The centered average actually converges to 0 in $L^2$, thus also in probability (Chung 2001, 108). Under the same hypotheses it also converges to 0 almost everywhere:

WLLN_uncorrelated_L2
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       (∀i j. i ≠ j ⇒ uncorrelated p (X i) (X j)) ∧
       (∃c. c ≠ +∞ ∧ ∀n. variance p (X n) ≤ c) ⇒
       LLN p X (in_lebesgue 2)

WLLN_uncorrelated
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       (∀i j. i ≠ j ⇒ uncorrelated p (X i) (X j)) ∧
       (∃c. c ≠ +∞ ∧ ∀n. variance p (X n) ≤ c) ⇒
       LLN p X in_probability

SLLN_uncorrelated
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       (∀i j. i ≠ j ⇒ uncorrelated p (X i) (X j)) ∧
       (∃c. c ≠ +∞ ∧ ∀n. variance p (X n) ≤ c) ⇒
       LLN p X almost_everywhere

For (pairwise) independent r.v.'s with identical distributions (also called I.I.D.), the Strong and Weak Law of Large Numbers are also proven:

WLLN_IID
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       pairwise_indep_vars p X (λn. Borel) 𝕌(:num) ∧
       identical_distribution p X Borel 𝕌(:num) ∧ integrable p (X 0) ⇒
       LLN p X in_probability

SLLN_IID
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       pairwise_indep_vars p X (λn. Borel) 𝕌(:num) ∧
       identical_distribution p X Borel 𝕌(:num) ∧ integrable p (X 0) ⇒
       LLN p X almost_everywhere

The Strong Law of Large Numbers (for I.I.D. r.v.'s) admits a converse in the following sense. If the expectation of each $|X_i|$ is infinite, then the upper limit of $\displaystyle\frac{|S_n|}{n}$ is almost-everywhere infinite (Chung 2001, 133):

SLLN_IID_diverge
  ⊢ ∀p X.
       prob_space p ∧ (∀n. real_random_variable (X n) p) ∧
       pairwise_indep_vars p X (λn. Borel) 𝕌(:num) ∧
       identical_distribution p X Borel 𝕌(:num) ∧
       expectation p (abs ∘ X 0) = +∞ ⇒
       AE x::p. limsup (λn. abs (∑ (λi. X i x) (count1 n)) / &SUC n) = +∞

Probability Theory (legacy)

The legacy probability theory based on real numbers (and all dependent theories) is still available from real_probability theory, to support legacy user code and two official examples (miller and diningcryptos). The whole theory consists of

real_measure
The $[0,+\infty)$-measure theory (based on real numbers).
real_borel
The theory of Borel sets and Borel measurable functions based on real numbers.11
real_lebesgue
The theory of Lebesgue integration based on real numbers.
real_probability
Probability theory based on real numbers.

Most theorems in the above theories have the same (or similar) name and statements with their extreal-based companies, to ease the difficulities when porting proofs to the new measure and probability theories.

In $(HOLDIR)/examples/miller, done by Hurd (Hurd 2002), first a type of Boolean sequences is defined to model an infinite sequence of coin flips. Next a probability function is formalized which takes as input a set of Boolean sequences, and returns a real number between 0 and 1. Building on this foundation, the probability theory is used to define a sampling function that takes an infinite sequence of coin flips and a positive integer $N$, and returns an integer $n$ in the range $0\le n < N$, picked uniformly at random from the available choices. This sampling function for the uniform distribution is later used to verify the Miller-Rabin primality test.

Other Theories of Higher Mathematics

In $(HOLDIR)/examples/algebra there is an abstract algebra library for HOL4. The algebraic types are generic, so the library is useful in general. The algebraic structures consist of

monoidTheory
for monoids with identity,
groupTheory
for groups,
ringTheory
for commutative rings (not to be confused with the other ring theory in core library),
fieldTheory
for fields,
polynomialTheory
for polynomials with coefficients from rings or fields,
linearTheory
for vector spaces, including linear independence, and
finitefieldTheory
for finite fields, including existence and uniqueness.

  1. Some contents here are taken from (Kaufmann, Manolios, and Moore 2000) (Section 6.3: The Ordinals). Courtesy of ACL2 developers.

  2. The derivativeTheory and integrationTheory were originally ported from HOL Light in 2015, by Muhammad Qasim and Osman Hasan et al. The corresponding original HOL Light theories are part of HOL Light's formalizations on multivariate calculus.

  3. Some contents below were taken from (Harrison 2017) and (Harrison 2012).

  4. There is also another closely related uniformly_continuous_on which implies continuous_on. See UNIFORMLY_CONTINUOUS_IMP_CONTINUOUS, COMPACT_UNIFORMLY_CONTINUOUS, etc. in real_topologyTheory.

  5. The legacy real_measure theory, where the type of measure is $(\alpha \to \konst{bool}) \to \konst{real}$, is still available.

  6. Previously these definitions were part of measure theory. Now they are moved into the shared sigma_algebra theory for both measure and real_measure theories.

  7. Some textbooks call sts itself a $\sigma$-algebra, while (sp,sts) a measurable space.

  8. Unlike the case of borel, Borel cannot be generated by bounded intervals such as $[a,b)$, because $+\infty$ and $-\infty$ cannot be separated by countable applications of $\cap$, $\cup$ and set-complement on bounded intervals.

  9. AE is defined as a restricted quantifier (binder). See associate_restriction in \REFERENCE.

  10. The legacy real_probability, where all probabilities are real-valued, is still available.

  11. real_borel is shared with extreal-based measure and probability theories. Also sigma_algebra is shared by both probability theories.

Advanced Definition Principles

General-Purpose Definition

Definition takes a high-level specification of a HOL function, and attempts to define the function in the logic. If this attempt is successful, the specification is derived from the definition. The derived specification is returned to the user, and also stored in the current theory. Definition may be used to define abbreviations, recursive functions, and mutually recursive functions.

The basic form of a Definition is

  Definition defname[attrs]:
    fn-spec
  End

or

  Definition defname[attrs]:
    fn-spec
  Termination
    tactic
  End

The latter form maps to a call to tDefine; the former to xDefine (or Define). (See their entries of in \REFERENCE{} for more details.) The termination argument is only needed when defining certain recursive functions. (See Section 7.6 for more details on the support of recursive functions, and mutually recursive functions and the full syntax of the fn-spec above.)

Note that this is not valid ML syntax. Instead, HOL is using lexical pre-processing to transform the above into something more complicated underneath. To make this reasonable, there are constraints introduced on the syntax above: all keywords (Definition, Termination and End) must appear in column 1 of the input (hard against the left margin in one's input source file).

In both cases, the defname is taken to be the name of the theorem stored to disk (it does not have a suffix such as _def appended to it), and is also the name of the local SML binding. The attributes given by attrs can be any standard attribute (such as simp), and/or drawn from Definition-specific options:

  • The attribute schematic allows the definition to be schematic. (See Section 7.6.3 for more details.)
  • The attribute nocompute stops the definition from being added to the global compset used by EVAL. This is equivalent to zDefine. (See Section 8.6 and also the corresponding entry in \REFERENCE{} for more details.)
  • The attribute induction=iname makes the induction theorem that is automatically derived for definitions with interesting termination be called iname. If this is omitted, the name chosen will be derived from the defname of the definition: if defname ends with _def or _DEF, the induction name will replace this suffix with _ind or _IND respectively; otherwise the induction name will simply be defname with _ind appended.

Note that, whether or not the induction= attribute is used, the induction theorem is always made available as an SML binding under the appropriate name. This means that one does not need to follow one's definition with a call to something like DB.fetch or theorem just to make the induction theorem available at the SML level.

Datatypes

Although the HOL logic provides primitive definition principles allowing new types to be introduced, the level of detail is very fine-grained. The style of datatype definitions in functional programming languages provides motivation for a high level interface for defining algebraic datatypes.

The Datatype function supports the definition of such data types; the specifications of the types may be recursive, mutually recursive, nested recursive, and involve records. The syntax of declarations that Datatype accepts is found in Table 7.2.1

Table: Datatype declaration syntax. An atomic-type is a single token that denotes a hol_type (e.g. num, real, or 'a).

Datatype `[binding ;]* binding`

binding          ::= ident = constructor-spec
                   | ident = record-spec
constructor-spec ::= [clause |]* clause
clause           ::= ident ty-spec*
ty-spec          ::= ( hol_type )  |  atomic-type
record-spec      ::= <| [ident : hol_type ;]* ident : hol_type ;? |>

HOL maintains an underlying database of datatype facts called the TypeBase. This database is used to support various high-level proof tools (see the bossLib section of the Libraries chapter), and is augmented whenever a Datatype declaration is made. When a datatype is defined by Datatype, the following information is derived and stored in the database.

  • initiality theorem for the type
  • injectivity of the constructors
  • distinctness of the constructors
  • structural induction theorem
  • case analysis theorem
  • definition of the ‘case’ constant for the type
  • congruence theorem for the case constant
  • definition of the ‘size’ of the type

When the HOL system starts up, the TypeBase already contains the relevant entries for the types bool, prod, num, option, and list.

Example: Binary trees

The following ML declaration of a data type of binary trees

  datatype ('a,'b) btree = Leaf of 'a
                         | Node of ('a,'b) btree * 'b * ('a,'b) btree

could be declared in HOL with a call to the Datatype function:

   Datatype `btree = Leaf 'a | Node btree 'b btree`

Also: good practice in script files is to make sure that everything is an ML declaration, so the above should really appear as

   val _ = Datatype `btree = Leaf 'a | Node btree 'b btree`

To reduce the verbiage of the above, there is a special syntax (akin to Theorem and Definition), allowing one to instead write

> Datatype:
    btree = Leaf 'a | Node btree 'b btree
  End
<<HOL message: Defined type: "btree">>

As with the other forms, the keywords here must be in column 1.

Note that in all forms, any type parameters for the new type are not mentioned: the type variables are always ordered alphabetically.

This subtle point bears repeating: the format of datatype definitions does not have enough information to always determine the order of arguments to the introduced type operators. Thus, when defining a type that is polymorphic in more than one argument, there is a question of what the order of the new operator's arguments will be. For another example, if one defines

   Datatype: sum = C1 'left | C2 'right
   End

and then writes ('a,'b)sum, will the 'a value be under the C1 or C2 constructor? The system chooses to make the arguments corresponding to variables appear in the order given by the dictionary ordering of the names of the variables occurring in the definition. Thus, in the example given, the 'a of ('a,'b)sum will be the argument to the C1 constructor because 'left comes before 'right in the standard (ASCII) dictionary ordering.

Further examples

In the following, we shall give an overview of the kinds of types that may be defined by Datatype.

To start, enumerated types can be defined as in the following example:

> Datatype:
    enum = A1  | A2  | A3  | A4  | A5
         | A6  | A7  | A8  | A9  | A10
         | A11 | A12 | A13 | A14 | A15
         | A16 | A17 | A18 | A19 | A20
         | A21 | A22 | A23 | A24 | A25
         | A26 | A27 | A28 | A29 | A30
  End
<<HOL message: Defined type: "enum">>

Other non-recursive types may be defined as well:

> Datatype:
    foo = N num | B bool | Fn ('a -> 'b) | Pr ('a # 'b)
  End
<<HOL message: Defined type: "foo">>

Turning to recursive types, we have already seen a type of binary trees having polymorphic values at internal nodes. This time, we will declare it in “paired” format.

> Datatype:
    tree = Leaf2 'a | Node2 (tree # 'b # tree)
  End
<<HOL message: Defined type: "tree">>

This specification seems closer to the declaration that one might make in ML, but can be more difficult to deal with in proof than the curried format used above.

The basic syntax of the named lambda calculus is easy to describe:

> Datatype:
    lambda = Var string
           | Const 'a
           | Comb lambda lambda
           | Abs lambda lambda
  End
<<HOL message: Defined type: "lambda">>

The syntax for ‘de Bruijn’ terms is roughly similar:

> Datatype:
    dB = Var2 string
       | Const2 'a
       | Bound num
       | Comb2 dB dB
       | Abs2 dB
  End
<<HOL message: Defined type: "dB">>

Arbitrarily branching trees may be defined by allowing a node to hold the list of its subtrees. In such a case, leaf nodes do not need to be explicitly declared.

> Datatype:
    ntree = Node3 'a (ntree list)
  End
<<HOL message: Defined type: "ntree">>

A type of ‘first order terms’ can be declared as follows:

> Datatype:
    term = Var3 string | Fnapp (string # term list)
  End
<<HOL message: Defined type: "term">>

Mutually recursive types may also be defined. The following, extracted by Elsa Gunter from the Definition of Standard ML, captures a subset of Core ML.

> Datatype:
     atexp = var_exp string
           | let_exp dec exp ;
  
       exp = aexp    atexp
           | app_exp exp atexp
           | fn_exp  match ;
  
     match = match  rule
           | matchl rule match ;
  
      rule = rule pat exp ;
  
       dec = val_dec   valbind
           | local_dec dec dec
           | seq_dec   dec dec ;
  
   valbind = bind  pat exp
           | bindl pat exp valbind
           | rec_bind valbind ;
  
       pat = wild_pat
           | var_pat string
  End
<<HOL message: Defined types: "pat", "atexp", "exp", "match", "rule", "dec", "valbind">>

Simple record types may be introduced using the <| ... |> notation.

> Datatype:
    state = <| Reg1 : num; Reg2 : num; Waiting : bool |>
  End
<<HOL message: Defined type: "state">>

The use of record types may be recursive. For example, the following declaration could be used to formalize a simple file system.

> Datatype:
     file = Text string | Dir directory
       ;
     directory = <| owner : string ;
                    files : (string # file) list |>
  End
<<HOL message: Defined types: "file", "directory">>

Type definitions that fail

Now we address some types that cannot be declared with Datatype. In some cases they cannot exist in HOL at all; in others, the type can be built in the HOL logic, but Datatype is not able to make the definition.

First, an empty type is not allowed in HOL, so the following attempt is doomed to fail.

> Datatype: tyfail1 = A tyfail1
  End
Exception- HOL_ERR
  (at Datatype.Datatype:
     at ind_types.define_type:
     at Lib.tryfind: all applications failed) raised

So called ‘nested types’, which are occasionally quite useful, cannot at present be built with Datatype:

> Datatype: tyfail2 = LeafX 'a | NodeX (('a # 'a) tyfail2)
  End
Exception- HOL_ERR
  (at Datatype.Datatype:
     at Datatype.to_tyspecs: Omit arguments to new type:"tyfail2") raised

Types may not recurse on either side of function arrows. Recursion on the right is consistent (see the theory inftree), but Datatype is not capable of defining algebraic types that require it. Thus, examples such as the following will fail:

> Datatype: tyfail3 = Nil | Cons 'a ('b -> tyfail3)
  End
Exception- HOL_ERR
  (at Datatype.Datatype:
     at ind_types.define_type:
     at ind_types.define_type_nested:
       Can't find definition for nested type: fun) raised

Recursion on the left must fail for cardinality reasons. For example, HOL does not allow the following attempt to model the untyped lambda calculus (note the -> in the clause for the Abs constructor):

> Datatype: tyfail4 = VarX string
                    | ConstX 'a
                    | CombX tyfail4 tyfail4
                    | AbsX (tyfail4 -> tyfail4)
  End
Exception- HOL_ERR
  (at Datatype.Datatype:
     at ind_types.define_type:
     at ind_types.define_type_nested:
       Can't find definition for nested type: fun) raised

Theorems arising from a datatype definition

The consequences of an invocation of Datatype are stored in the current theory segment and in TypeBase. The principal consequences of a datatype definition are the primitive recursion and induction theorems. These provide the ability to define simple functions over the type, and an induction principle for the type. Thus, for a type named ty, the primitive recursion theorem is stored under ty_Axiom and the induction theorem is put under ty_induction. Other consequences include the distinctness of constructors (ty_distinct), and the injectivity of constructors (ty_11). A ‘degenerate’ version of ty_induction is also stored under ty_nchotomy: it provides for reasoning by cases on the construction of elements of ty. Finally, some special-purpose theorems are stored: for example, ty_case_cong holds a congruence theorem for “case” statements on elements of ty. These case statements are defined by ty_case_def. Also, a definition of the “size” of the type is added to the current theory, under the name ty_size_def.

For example, invoking

> Datatype:
    ntree2 = NLeaf num | NNode ntree2 ntree2
  End
<<HOL message: Defined type: "ntree2">>

results in the definitions

> val ntree2_case_def = DB.fetch "-" "ntree2_case_def";
  val ntree2_size_def = DB.fetch "-" "ntree2_size_def";
val ntree2_case_def =
   ⊢ (∀a f f1. ntree2_CASE (NLeaf a) f f1 = f a) ∧
     ∀a0 a1 f f1. ntree2_CASE (NNode a0 a1) f f1 = f1 a0 a1: thm
val ntree2_size_def =
   ⊢ (∀a. ntree2_size (NLeaf a) = 1 + a) ∧
     ∀a0 a1.
       ntree2_size (NNode a0 a1) = 1 + (ntree2_size a0 + ntree2_size a1): thm

being added to the current theory. Note that the generated theorems are not bound at the SML top level by default; the reader has to retrieve them from the theory database explicitly, as the DB.fetch "-" "...name..." calls above illustrate. The case constant (here ntree2_CASE) allows pretty case expressions; see Section 7.5 below. The following theorems about the datatype are also proved and stored in the current theory.

> val ntree2_Axiom     = DB.fetch "-" "ntree2_Axiom";
  val ntree2_induction = DB.fetch "-" "ntree2_induction";
  val ntree2_nchotomy  = DB.fetch "-" "ntree2_nchotomy";
  val ntree2_11        = DB.fetch "-" "ntree2_11";
  val ntree2_distinct  = DB.fetch "-" "ntree2_distinct";
  val ntree2_case_cong = DB.fetch "-" "ntree2_case_cong";
val ntree2_Axiom =
   ⊢ ∀f0 f1. ∃fn.
       (∀a. fn (NLeaf a) = f0 a) ∧
       ∀a0 a1. fn (NNode a0 a1) = f1 a0 a1 (fn a0) (fn a1): thm
val ntree2_induction =
   ⊢ ∀P. (∀n. P (NLeaf n)) ∧ (∀n n0. P n ∧ P n0 ⇒ P (NNode n n0)) ⇒ ∀n. P n:
   thm
val ntree2_nchotomy = ⊢ ∀nn. (∃n. nn = NLeaf n) ∨ ∃n n0. nn = NNode n n0: thm
val ntree2_11 =
   ⊢ (∀a a'. NLeaf a = NLeaf a' ⇔ a = a') ∧
     ∀a0 a1 a0' a1'. NNode a0 a1 = NNode a0' a1' ⇔ a0 = a0' ∧ a1 = a1': thm
val ntree2_distinct = ⊢ ∀a1 a0 a. NLeaf a ≠ NNode a0 a1: thm
val ntree2_case_cong =
   ⊢ ∀M M' f f1.
       M = M' ∧ (∀a. M' = NLeaf a ⇒ f a = f' a) ∧
       (∀a0 a1. M' = NNode a0 a1 ⇒ f1 a0 a1 = f1' a0 a1) ⇒
       ntree2_CASE M f f1 = ntree2_CASE M' f' f1': thm

When a type involving records is defined, many more definitions are made and added to the current theory.

A mutually recursive type definition results in the above theorems and definitions being added for each of the defined types.

Record Types

Record types are convenient ways of bundling together a number of component types, and giving those components names so as to facilitate access to them. Record types are semantically equivalent to big pair (product) types, but the ability to label the fields with names of one's own choosing is a great convenience. Record types as implemented in HOL are similar to C's struct types and to Pascal's records.

Done correctly, record types provide useful maintainability features. If one can always access the fieldn field of a record type by simply writing record.fieldn, then changes to the type that result in the addition or deletion of other fields will not invalidate this reference. One failing in SML's record types is that they do not allow the same maintainability as far as (functional) updates of records are concerned. The HOL implementation allows one to write

  rec with fieldn := new_value

which replaces the old value of fieldn in the record rec with new_value. This expression will not need to be changed if another field is added, modified or deleted from the record's original definition.

Defining a record type

Record types are defined with the function Datatype, as previously discussed. For example, to create a record type called person with boolean, string and number fields called employed, name and age, one would enter:

> Datatype:
    person = <| employed : bool ; age : num ; name : string |>
  End
<<HOL message: Defined type: "person">>

The order in which the fields are entered is not significant. As well as defining the type (called person), the datatype definition function also defines two other sets of constants. These are the field access functions and functional update functions.

The field access functions have names of the form record-type_field. These functions can be used directly, or one can use standard field selection notation to access the values of a record's field. Thus, one would write the expression bob.employed in order to return the value of bob's employed field. The alternative, person_employed bob, works, but would be printed using the first syntax, with the full-stop.

The functional update functions are given the names "record-type_field_fupd" for each field in the type. They take two arguments, a function and a record to be updated. The function parameter is an endomorphism on the field type, so that the resulting record is the same as the original, except that the specified field has had the given function applied to it to generate the new value for that field. They can be written with the keyword with and the updated_by operator. Thus

> ``bob with employed updated_by $~``;
val it = “bob with employed updated_by $¬”: term

is a record value identical to the bob except that the boolean value in the employed field has been inverted. (bob here is just a free HOL variable of type person; we don't need to bind it to a specific value to talk about this syntax.)

Additionally, there is syntactic sugar available to let one write a record with one of its fields replaced by a specific value. This is done by using the := operator instead of updated_by:

> ``bob with employed := T``;
val it = “bob with employed := T”: term

This form is translated at parse-time to be a use of the corresponding functional update, along with a use of the $\mathsf{K}$-combinator from the combin theory. Thus, the above example is really

> ``bob with employed updated_by (K T)``;
val it = “bob with employed := T”: term

which is in turn a pretty form of

> ``person_employed_fupd (K T) bob``;
val it = “bob with employed := T”: term

If a chain of updates is desired, then multiple updates can be specified inside <|-|> pairs, separated by semi-colons, thus:

> ``bob with <| age := 10; name := "Child labourer" |>``;
val it = “bob with <|age := 10; name := "Child labourer"|>”: term

Both update forms (using updated_by and :=) can be used in a chain of updates.

Specifying record literals

The parser accepts lists of field specifications between <|-|> pairs without the with keyword. These translate to sequences of updates of an arbitrary value (literally, the HOL value ARB), and are treated as literals. Thus,

> ``<| age := 21; employed := F; name := "Layabout" |>``;
val it = “<|age := 21; employed := F; name := "Layabout"|>”: term

Using the theorems produced by record definition

As well as defining the type and the functions described above, record type definition also proves a suite of useful theorems. These are all saved (using save_thm) in the current segment.

Some are also added to the TypeBase's simplifications for the type, so they will be automatically applied when simplifying with the srw_ss() simpset, or with the tactics RW_TAC and SRW_TAC (see Section 8.5).

All of the theorems are saved under names that begin with the name of the type. The list below is a sample of the theorems proved. The identifying strings are suffixes appended to the name of the type in order to generate the final name of the theorem.

_accessors
The definitions of the accessor functions. This theorem is installed in the TypeBase.
_fn_updates
The definitions of the functional update functions.
_accfupds
A theorem that states simpler forms for expressions that are of the form $\mathit{field}_i\,(\mathit{field}_j\mathtt{\_fupd}\;f\;r)$. If $i = j$, then the RHS is $f(\mathit{field}_i(r))$, if not, it is $(\mathit{field}_i\;r)$. This theorem is installed in the TypeBase.
_component_equality
A theorem stating that $(r_1 = r_2) \equiv \bigwedge_i (\mathit{field}_i(r_1) = \mathit{field}_i(r_2))$.
_fupdfupds
A theorem stating that $\mathit{field}_i\mathtt{\_fupd}\;f\,(\mathit{field}_i\mathtt{\_fupd}\;g\;r) = \mathit{field}_i\mathtt{\_fupd}\;(f \circ g)\;r$. This theorem is installed in the TypeBase.
_fupdcanon
A theorem that states commutativity results for all possible pairs of field updates. They are constructed in such a way that if used as rewrites, they will canonicalise sequences of updates. Thus, for all $i < j$, the equation $\mathit{field}_j\mathtt{\_fupd}\;f\;(\mathit{field}_i\mathtt{\_fupd}\;g\;r)$ $=$ $\mathit{field}_i\mathtt{\_fupd}\;g\;(\mathit{field}_j\mathtt{\_fupd}\;f\;r)$ is generated. This theorem is installed in the TypeBase.

Big records

The size of certain theorems proved in the record type package increases as the square of the number of fields in the record. (In particular, the update canonicalisation and acc_fupd theorems have this property.) To avoid this problem, users should define a system of hierarchically nested sub-records if their records are getting too large.

Quotient Types

HOL provides a library for defining new types which are quotients of existing types, with respect to partial equivalence relations. This library is described in “Higher Order Quotients in Higher Order Logic” [HOQ], from which the following description is taken.

The quotient library is accessed by opening quotientLib, which makes all its tools and theorems accessible.

The definition of new types corresponding to the quotients of existing types by equivalence relations is called “lifting” the types from a lower, more representational level to a higher, more abstract level. Both levels describe similar objects, but some details which are apparent at the lower level are no longer visible at the higher level. The logic is simplified.

Simply forming a new type does not complete the quotient operation. Rather, one wishes to recreate the pre-existing logical environment at the new, higher, and more abstract level. This includes not only the new types, but also new versions of the constants that form and manipulate values of those types, and also new versions of the theorems that describe properties of those constants. All of these form a logical layer, above which all the lower representational details may be safely and forever forgotten.

This can be done in a single call of the main tool of this package.

define_quotient_types :
        {types: {name: string,
                 equiv: thm} list,
         defs: {def_name: string,
                fname: string,
                func: Term.term,
                fixity: Parse.fixity} list,
         tyop_equivs : thm list,
         tyop_quotients : thm list,
         tyop_simps : thm list,
         respects : thm list,
         poly_preserves : thm list,
         poly_respects : thm list,
         old_thms : thm list} ->
        thm list

define_quotient_types takes a single argument which is a record with the following fields.

types is a list of records, each of which contains two fields: name, which is the name of a new quotient type to be created, and equiv, which is either (1) a theorem that a binary relation $R$ is an equivalence relation (see [HOQ] §4) of the form

$$\mathtt{|-}\ \forall x\ y.\ R\ x\ y \Leftrightarrow (R\ x = R\ y),$$

or (2) a theorem that $R$ is a nonempty partial equivalence relation, (see [HOQ] §5) of the form

$$\mathtt{|-}\ (\exists x.\ R\ x\ x) \wedge (\forall x\ y.\ R\ x\ y \Leftrightarrow R\ x\ x \wedge R\ y\ y \wedge (R\ x = R\ y)).$$

The process of forming the new quotient types is described in [HOQ] §8.

defs is a list of records specifying the constants to be lifted. Each record contains the following four fields: func is an HOL term, which must be a single constant, which is the constant to be lifted. fname is the name of the new constant being defined as the lifted version of func. fixity is the HOL fixity of the new constant being created, as specified in the HOL structure Parse. def_name is the name under which the new constant definition is to be stored in the current theory. The process of defining lifted constants is described in [HOQ] §9.

tyop_equivs is a list of conditional equivalence theorems for type operators (see [HOQ] §4.1). These are used for bringing into regular form theorems on new type operators, so that they can be lifted (see [HOQ] §11 and §12).

tyop_quotients is a list of conditional quotient theorems for type operators (see [HOQ] §5.2). These are used for lifting both constants and theorems.

tyop_simps is a list of theorems used to simplify type operator relations and map functions, e.g., for pairs, |- ($= ### $=) = $= and |- (I ## I) = I.

The rest of the arguments refer to the general process of lifting theorems over the quotients being defined, as described in [HOQ] §10.

respects is a list of theorems about the respectfulness of the constants being lifted. These theorems are described in [HOQ] §10.1.

poly_preserves is a list of theorems about the preservation of polymorphic constants in the HOL logic across a quotient operation. In other words, they state that any quotient operation preserves these constants as a homomorphism. These theorems are described in [HOQ] §10.2.

poly_respects is a list of theorems showing the respectfulness of the polymorphic constants mentioned in poly_preserves. These are described in [HOQ] §10.3.

old_thms is a list of theorems concerning the lower, representative types and constants, which are to be automatically lifted and proved at the higher, more abstract quotient level. These theorems are described in [HOQ] §10.4.

define_quotient_types returns a list of theorems, which are the lifted versions of the old_thms.

A similar function, define_quotient_types_rule, takes a single argument which is a record with the same fields as above except for old_thms, and returns an SML function of type thm -> thm. This result, typically called LIFT_RULE, is then used to lift the old theorems individually, one at a time.

For backwards compatibility with the excellent quotients package EquivType created by John Harrison (which provided much inspiration), the following function is also provided:

define_equivalence_type :
        {name: string,
         equiv: thm,
         defs: {def_name: string,
                fname: string,
                func: Term.term,
                fixity: Parse.fixity} list,
         welldefs : thm list,
         old_thms : thm list} ->
        thm list

This function is limited to a single quotient type, but may be more convenient when the generality of define_quotient_types is not needed. This function is defined in terms of define_quotient_types as

fun define_equivalence_type {name,equiv,defs,welldefs,old_thms} =
    define_quotient_types
     {types=[{name=name, equiv=equiv}], defs=defs, tyop_equivs=[],
      tyop_quotients=[FUN_QUOTIENT],
      tyop_simps=[FUN_REL_EQ,FUN_MAP_I], respects=welldefs,
      poly_preserves=[FORALL_PRS,EXISTS_PRS],
      poly_respects=[RES_FORALL_RSP,RES_EXISTS_RSP],
      old_thms=old_thms};

Case Expressions

Case constructs are an important feature of functional programming languages such as Standard ML. They provide a very compact and convenient notation for multi-way selection among the values of several expressions. HOL provides such a feature in the form of case expressions. Case expressions can simplify the expression of complicated branches between different cases or combinations of cases.

Pattern matching and case expressions are not directly supported by higher order logic. Thus, some effort is needed to support them in HOL. There are two implementations of case expressions in HOL. The parser supports case expressions, which it compiles into decision trees based on if-then-else expressions and case-constants as introduced by datatype definitions. The pretty-printer presents these complicated decision trees as nicely readable case expressions again. In addition, there is patternMatchesLib, which provides a formalisation of case expressions based on Hilbert's choice operator. Following the name of the main function, these are called Pmatch case expressions.

The benefit of decision-tree case expressions is that the whole semantic complexity is handled by the parser and pretty-printer. The resulting terms are simple. However, one needs to trust the complicated, lengthy case expression code in the parser and pretty-printer. Moreover, the internal structure might differ considerably from the input and is hard to predict. In some cases there can be a serious blow-up in size.

Pmatch case expressions are simple to parse and pretty-print. They support more features than decision-tree ones. There are guards, unbound variables in patterns and a wider variety of supported patterns in general. There is no size-blowup or surprising internal structure. Therefore, code generated from Pmatch case expressions tends to be better. However, this comes at the price of a much heavier machinery to reason about Pmatch case expressions.

Decision-tree case expressions

By default, HOL uses decision-tree case expressions. Let the non-terminal term stand for any HOL term and the non-terminal cpat represent any constructor pattern, i.e., any HOL term containing only literals, datatype constructors and variables. Then, the syntax of decision-tree case expressions is given by

$$\mathit{term} ::= \mathtt{case}\;\mathit{term}\;\mathtt{of}\;\mathtt{|}^?\; \mathit{cpat}\;\mathtt{=>}\;\mathit{term}\; (\mathtt{|}\;\mathit{cpat}\;\mathtt{=>}\;\mathit{term})^*$$

The choice in the rule allows the use of more uniform syntax, where every case is preceded by a vertical bar. Omitting the bar, which is what the pretty-printer does when the syntax is printed, conforms to the syntax used by SML.

Case expressions consider their list of pattern expressions in sequence to see if they match the test expression. Matching means that there is an assignment for the bound variables of that pattern expressions such that it equals the test expression. The first pattern which successfully matches causes its associated result expression to be evaluated with the matching variable assignment. The resulting value is yielded as the value of the entire case expression. If no pattern expression matches, the result of the case expression is ARB. Since decision-tree case expressions support only constructor patterns, it is guaranteed that if a pattern expression matches, the resulting variable assignment is uniquely determined.

A simple example of a case expression is

case n of
    0 => "none"
  | 1 => "one"
  | 2 => "two"
  | _ => "many"

This could have been expressed using several “if-then-else” constructs, but the case expression is much more compact and clean, with the selection between various choices made clearly evident. Internally though, it is compiled to nested “if-then-else” statements.

In addition to literals as patterns, as above, patterns may be constructor expressions. Many standard HOL types have constructors, including num, list, and option. A simple example using constructor patterns is (notice the optional bar in front of the first case).

case spouse(employee) of
  | NONE   => "single"
  | SOME s => "married to " ++ name_of s

HOL supports a rich structure of case expressions using a single notation. The format is related to that of definitions of recursive functions, as described in Section 7.6. In addition, case expressions may contain literals as patterns, either singly or as elements of deeply nested patterns.

Decision-tree case expressions may test values of any type. If the test expression is a type with constructors, then the patterns may be expressed using the constructors applied to arguments, as for example SOME s in the example above. A free variable within the constructor pattern, for example s in the pattern SOME s, becomes bound to the corresponding value within the value of the test expression, and can be used within the associated result expression for that pattern.

In addition to the constructors of standard types in HOL, constructor patterns may also be used for types created by use of the datatype definition facility described in Section 7.2, including user-defined types.

Whether or not the test expression is a type with constructors, the patterns may be expressed using the appropriate literals of that type, if any such literals exist. A complex pattern may contain either or both of literals and constructor patterns nested within it. However, literals and constructors may not be mixed as alternatives of each other within the same case expression, except insofar as a particular pattern may be both a literal and also a (0-ary) constructor of its type, as for example 0 (zero) is both a literal and a constructor of the type num. The session below demonstrates the way in which such an improper mixture is misinterpreted. In this pattern, the constructor pattern SUC m is given as an alternative to the literal patterns 1 and 2. This makes this attempted case expression invalid. Deleting either group of rows would resolve the conflict, and make the expression valid. Note that the pattern 0 is acceptable to either group.

> “case n of
      0 => "none"
    | 1 => "one"
    | 2 => "two"
    | SUC m => "many"”;
Exception- HOL_ERR
  (at Pmatch.mk_case: case expression mixes literals with non-literals.) raised

Patterns can be nested as well, as shown in the first example below, where the function parents returns a pair containing the person's father and/or mother, where each is represented by NONE if deceased. This shows the nesting of option patterns within a pair pattern, and also the use of a wildcard _ to match the cases not given.

case parents(john) of
   (NONE,NONE) => "orphan"
 | _ => "not an orphan"

A second example, using nested literal patterns and a wildcard in each row:

case a of
     (1, y, z) => y + z
   | (x, 2, z) => x - z
   | (x, y, 3) => x * y

Since decision-tree case expressions are compiled internally to a decision tree, the result of this compilation might look quite different from the input. Rows might be reordered or modified, there might be several new rows generated, and new variables or the ARB constant may also be introduced to properly represent the case expression. Moreover, the exact result depends on complicated heuristics to decide which case-split to perform next in the decision tree. For non-trivial case expressions the result can be hard to predict.

For example, consider the second case expression above. Using the default heuristic, this compiles to a reasonably small decision tree that is pretty-printed well. The exact combination of equality tests with if-then-else and the case constant for products that this term produces can be seen by turning off the case-expression pretty-printer:

> set_trace "pp_cases" 0;   ... output elided ...
> “case a of
      (1,y,z) => y + z
    | (x,2,z) => x - z
    | (x,y,3) => x * y”;
<<HOL message: mk_functional: 
  pattern completion has added 1 clause to the original specification.>>
val it =
   “pair_CASE a
      (λv v1.
           pair_CASE v1
             (λy z.
                  literal_case
                    (λx.
                         if x = 1 then y + z
                         else
                           literal_case
                             (λy'.
                                  if y' = 2 then x − z
                                  else
                                    literal_case
                                      (λv10. if v10 = 3 then x * y' else ARB)
                                      z) y) v))”: term

Other heuristics result in more complicated terms:

> Pmatch.set_classic_heuristic ();   ... output elided ...
> “case a of
      (1,y,z) => y + z
    | (x,2,z) => x - z
    | (x,y,3) => x * y”;
<<HOL message: mk_functional: 
  pattern completion has added 5 clauses to the original specification.>>
val it =
   “case a of
      (1,2,3) => 2 + 3
    | (1,2,v10) => 2 + v10
    | (1,y,3) => y + 3
    | (1,y,z) => y + z
    | (x,2,3) => x − 3
    | (x,2,z') => x − z'
    | (x,y',3) => x * y'
    | (x,y',v24) => ARB”: term
> PmatchHeuristics.set_heuristic PmatchHeuristics.pheu_last_col;   ... output elided ...
> “case a of
      (1,y,z) => y + z
    | (x,2,z) => x - z
    | (x,y,3) => x * y”;
<<HOL message: mk_functional: 
  pattern completion has added 2 clauses to the original specification.>>
val it =
   “case a of
      (1,y,z) => y + z
    | (x,2,3) => x − 3
    | (x,y',3) => x * y'
    | (x,2,z') => x − z'
    | (x,v13,z') => ARB”: term
> Pmatch.set_default_heuristic ();   ... output elided ...
> “case a of
      (1,y,z) => y + z
    | (x,2,z) => x - z
    | (x,y,3) => x * y”;
<<HOL message: mk_functional: 
  pattern completion has added 1 clause to the original specification.>>
val it =
   “case a of
      (1,y,z) => y + z
    | (x,2,z) => x − z
    | (x,y',3) => x * y'
    | (x,y',v10) => ARB”: term

This is just a brief description of some of the expressive capabilities of the case expression with patterns. Many more examples of patterns are provided in Section 7.6 on the definition of recursive functions.

Pmatch case expressions

The library patternMatchesLib supports another form of case expression. If pmatch syntax is enabled (via patternMatchesSyntax.temp_enable_pmatch() or enable_pmatch()), the pmatch keyword can be used to parse Pmatch-based terms. If we turn off the library's pretty-printing, we can see how the example expression we used earlier is rendered:

> patternMatchesSyntax.temp_enable_pmatch();   ... output elided ...
> set_trace "use pmatch_pp" 0;   ... output elided ...
> “pmatch a of
     (1, y, z) => y + z
   | (x, 2, z) => x - z
   | (x, y, 3) => x * y”;
val it =
   “PMATCH a
      [PMATCH_ROW (λ(y,z). (1,y,z)) (λ(y,z). T) (λ(y,z). y + z);
       PMATCH_ROW (λ(x,z). (x,2,z)) (λ(x,z). T) (λ(x,z). x − z);
       PMATCH_ROW (λ(x,y). (x,y,3)) (λ(x,y). T) (λ(x,y). x * y)]”: term

One can see that in contrast to decision-tree case expressions the internal representation of Pmatch case expressions is very close to the input. No fancy parsing is required to turn the input into the internal representation, and conversely, printing (when enabled) can easily produce the syntax that the user chose as input.

Pmatch case expressions are more expressive than decision-tree based ones. The syntax allowed for decision-tree case expressions is a subset of the syntax of Pmatch ones. In addition, Pmatch case expressions support guards. Moreover, arbitrary terms instead of just ones using literals and constructors can be used as patterns. One has full control over the variables bound by patterns. Bound variables can even be used more than once in a pattern.

$$ \begin{aligned} \mathit{term} & ::= \mathtt{pmatch}\;\mathit{term}\;\mathtt{of}\;\mathtt{|}^?\;\mathit{clause}\;(\mathtt{|}\;\mathit{clause})^*\\ \mathit{clause} & ::= \mathit{vardecl}^?\;\mathit{term}\; (\mathtt{when}\;\mathit{term})^? \;\mathtt{=>}\; \mathit{term} \\ \mathit{vardecl} & ::= \mathtt{(}\mathit{vars}^?\mathtt{)}\;\mathtt{.|}\;\;\;\mid\;\;\;\mathit{vars}\;\mathtt{.|}\\ \mathit{vars} & ::= \mathit{var}\;\;\;\mid\;\;\;\mathit{var}\;\mathtt{,}\;\mathit{vars} \end{aligned} $$

If a when-guard is omitted, it defaults to true (T). Omitting the declaration of variables bound by a pattern means that all variables used in the pattern are bound (as with the decision-tree syntax). Variables whose names start with an underscore are always bound, no matter whether they appear in the list of bound variables. This is convenient for using wildcard notations.

These extensions of the basic case expression syntax allow the expression of some interesting concepts using case expressions. One can for example express division with remainder using Pmatch case expressions:

!n c.
  0 < c ==>
  ((pmatch n of (q,r) .| q * c + r when r < c => (q,r)) =
   (n DIV c,n MOD c))

Notice that

  • the guard is used to make the match unique;
  • listing the bound variables of the pattern q * c + r explicitly allows c to not be bound by the pattern; and
  • the pattern (of form $qc + r$) would not be supported by decision-tree case expressions because it is not a constructor pattern.

Another interesting option is to use bound variables multiple times. The following case expression states for example that a list l starts with two copies of the same element:

pmatch l of | x::x::_ => T | _ => F

The price for this increased expressive power and the simpler, more trustworthy parsing and pretty-printing is that the semantic complexity now needs to be dealt with inside the logic. The patternMatchesLib library provides the necessary machinery in the form or conversions, rules, simpsets, and other technology. For example, patternMatchesLib enhances bossLib's standard simpsets with the conversions to deal with Pmatch case expressions. Therefore, in practice Pmatch case expressions are as convenient to use as decision-tree based case expressions. In fact, the explicit case expression structure provided by Pmatch case expressions allows simplifications not easily possible for decision-tree case expressions. The standard tools can only (partially) evaluate decision-tree case expressions. This evaluation usually destroys the case expression view. This is illustrated by the following example, which adds a row that is subsumed by a later one to our running example:

> SIMP_CONV (srw_ss()) []
    “pmatch (x,y,z) of
       (1,y,z) => y + z
     | (x,2,4) => x - 4  (* subsumed by next row *)
     | (x,2,z) => x - z
     | (x,y,3) => x * y”;
val it =
   ⊢ (pmatch (x,y,z) of
       (1,y,z) => y + z
     | (x,2,4) => x − 4
     | (x,2,z) => x − z
     | (x,y,3) => x * y) =
     pmatch (x,y,z) of (1,y,z) => y + z | (x,2,z) => x − z | (x,y,3) => x * y:
   thm
> SIMP_CONV (srw_ss()) []
   “case (x,y,z) of
      (1,y,z) => y + z
    | (x,2,4) => x - 4
    | (x,2,z) => x - z
    | (x,y,3) => x * y”;
<<HOL message: mk_functional: 
  pattern completion has added 2 clauses to the original specification.>>
val it =
   ⊢ (case (x,y,z) of
        (1,y,z) => y + z
      | (x,2,4) => x − 4
      | (x,2,3) => x − 3
      | (x,2,z') => x − z'
      | (x,y',3) => x * y'
      | (x,y',v14) => ARB) =
     if x = 1 then y + z
     else if y = 2 then
       if z = 4 then x − 4 else if z = 3 then x − 3 else x − z
     else if z = 3 then x * y
     else ARB: thm

There are many more tools for working with Pmatch case expressions. There are, for example, tools for removing multiple variable binding or guards. One can translate between Pmatch and decision-tree case expressions. There are tools for proving exhaustiveness of Pmatch case expressions or for removing redundant rows. For more details, please consult the patternMatchesLib documentation in Section 8.9.

Recursive Functions

HOL provides a function definition mechanism based on the wellfounded recursion theorem proved in relationTheory, discussed in Section 5.5.3. With the Definition syntax, users provide a high-level, possibly recursive, specification of a function, and HOL attempts to define the function in the logic. This technology may be used to define abbreviations, recursive functions, and mutually recursive functions. An induction theorem may be generated as a by-product of this activity. This induction theorem follows the recursion structure of the function, and may be useful when proving properties of the function. The definition technology is not always successful in attempting to make the specified definition, usually because an automatic termination proof fails; in that case, another entrypoint, Hol_defn, which defers the termination proof to the user, can be used. Once a termination argument is found, it can be provided as part of the final Definition syntax (See Section 7.1 for more details.) The technology underlying Definition and Hol_defn is explained in detail in Slind (Slind 1999). For example,

> Definition last0_def:
    (last0 [] = 0) /\
    (last0 [x] = x) /\
    (last0 (h::t) = last0 t)
  End
Equations stored under "last0_def".
Induction stored under "last0_ind".
val last0_def =
   ⊢ last0 [] = 0 ∧ (∀x. last0 [x] = x) ∧
     ∀v3 v2 h. last0 (h::v2::v3) = last0 (v2::v3): thm

The fn-spec in the syntax of Definition and Hol_defn is quoted syntax representing a conjunction of equations. The specified function(s) may be phrased using ML-style pattern-matching. The fn-spec should conform with the grammar in Table 7.6.

$$ \begin{aligned} \mathit{fn\text{-}spec} & ::= \mathit{eqn} \\ & \;\mid\; (\mathit{eqn}) \wedge \mathit{fn\text{-}spec} \\[2pt] \mathit{eqn} & ::= \mathit{alphanumeric}\ \mathit{pat} \ldots \mathit{pat} = \mathit{term} \\[2pt] \mathit{pat} & ::= \mathit{variable} \\ & \;\mid\; \mathit{wildcard} \\ & \;\mid\; \mathit{cname} \\ & \;\mid\; (\mathit{cname}_n\ \mathit{pat}_1 \ldots \mathit{pat}_n) \\[2pt] \mathit{cname} & ::= \mathit{alphanumeric}\ \mid\ \mathit{symbolic} \\[2pt] \mathit{wildcard} & ::= \mathtt{\_\_} \\ & \;\mid\; \mathtt{\_\_}\,\mathit{wildcard} \end{aligned} $$

Table: Syntax of Function Declaration.

Pattern expansion

In general, Definition attempts to derive exactly the specified conjunction of equations. However, the rich syntax of patterns allows some ambiguity. For example, the input

Definition f_def:
  (f 0 _ = 1) /\
  (f _ 0 = 2)
End

is ambiguous at f 0 0: should the result be 1 or 2? This ambiguity is dealt with in the usual way for compilers and interpreters for functional languages: namely, the conjunction of equations is treated as being applied left-conjunct first, followed by processing the right conjunct. Therefore, in the example above, the value of f 0 0 is 1. In the implementation, ambiguities arising from such overlapping patterns are systematically translated away in a pre-processing step.

Another case of ambiguity in patterns is shown above: the specification is incomplete since it does not tell how f should behave when applied to two non-zero arguments: e.g., f (SUC m) (SUC n). In the implementation, such missing clauses are filled in, and have the value ARB. This `pattern-completion' step is a way of turning descriptions of partial functions into total functions suitable for HOL. However, since the user has not completely specified the function, the system takes that as a hint that the user is not interested in using the function at the missing-but-filled-in clauses, and so such clauses are dropped from the final theorem.

In summary, Definition will derive the unambiguous and complete equations

f 0 v0 = 1 /\ f (SUC v3) 0 = 2 /\ f (SUC v1) (SUC v2) = ARB

from the above ambiguous and incomplete equations. The odd-looking variable names are due to the pre-processing steps described above. Depending on the analysis of the pattern-matching (see Section 7.5 for how this is done, and how it can be tweaked), the theorem returned from a definition may look rather unlike the original input. In this particular case, the post-processing needs only drop the last conjunct, which was not part of the original user-specification, yielding:

val f_def = ⊢ ∀v3 v0. f 0 v0 = 1 ∧ f (SUC v3) 0 = 2: thm

Termination

When processing the specification of a recursive function, Definition must perform a termination proof. It automatically constructs termination conditions for the function, and invokes a termination prover in an attempt to prove the termination conditions. If the function is primitive recursive, in the sense that it exactly follows the recursion pattern of a previously declared HOL datatype, then this proof always succeeds, and Definition stores the derived equations in the current theory segment. Otherwise, the function is not an instance of primitive recursion, and the termination prover may succeed or fail. If the automatic termination proof fails, the user-provided termination argument (if any) after the Termination keyword is attempted. If this also fails, then the Definition fails. If it succeeds, then Definition stores the specified equations in the current theory segment. An induction theorem customized for the defined function is also stored in the current segment. Note, however, that an induction theorem is not stored for primitive recursive functions, since that theorem would be identical to the induction theorem resulting from the declaration of the datatype.

Part of the process of the automatic termination proof involves the use of “termination simplification” theorems. These encode facts about the way in which arguments to desired functions may be smaller than originals. For example, with the theory of fixed-width words (see Section 5.3.8) in the context, the theorem WORD_PRED_THM is included in this set:

wordsTheory.WORD_PRED_THM
  ⊢ ∀m. m ≠ words$n2w 0 ⇒
         words$w2n (words$word_sub m (words$n2w 1)) < words$w2n m

If a function is defined recursively with a word argument being decremented with every call, including this theorem will let the automatic procedure prove termination. The set of theorems used in this way can be augmented by using the tfl_termsimp theorem attribute when a theorem is proved.

Storing definitions in the theory segment

The name given in the use of the Definition syntax is used both to store the definition in the current theory and is also bound to the same theorem within the ML environment. If there is an associated induction theorem, its name is derived as follows:

  • If the principal name ends with _def or with _DEF, then the induction theorem is the same as the principal name with the suffix replaced by _ind or _IND respectively.
  • Otherwise, the induction theorem is the principal name with _ind added as a suffix.
  • Finally, the user may override these choices by adding an induction_thm=name attribute to the principal name.

Function definition examples

We will give a number of examples that display the range of functions that may be defined with Definition. First, we have a recursive function that uses “destructors” in the recursive call.

> Definition fact_def:
    fact x = if x = 0 then 1 else x * fact(x-1)
  End
Equations stored under "fact_def".
Induction stored under "fact_ind".
val fact_def = ⊢ ∀x. fact x = if x = 0 then 1 else x * fact (x − 1): thm

Since fact is not primitive recursive, an induction theorem for fact is generated and stored in the current theory.

> fact_ind; (* DB.fetch "-" "fact_ind" would also work *)
val it = ⊢ ∀P. (∀x. (x ≠ 0 ⇒ P (x − 1)) ⇒ P x) ⇒ ∀v. P v: thm

Next we have a recursive function with relatively complex pattern-matching. We use the induction= “attribute” to require a non-standard name for the generated induction theorem.

> Definition flatten[induction=flat_ind]:
     (flatten  []           = [])
  /\ (flatten ([]::rst)     = flatten rst)
  /\ (flatten ((h::t)::rst) = h::flatten(t::rst))
  End
<<HOL message: inventing new type variable names: 'a>>
Equations stored under "flatten".
Induction stored under "flat_ind".
val flatten =
   ⊢ flatten [] = [] ∧ (∀rst. flatten ([]::rst) = flatten rst) ∧
     ∀t rst h. flatten ((h::t)::rst) = h::flatten (t::rst): thm
> flat_ind;
val it =
   ⊢ ∀P. P [] ∧ (∀rst. P rst ⇒ P ([]::rst)) ∧
         (∀h t rst. P (t::rst) ⇒ P ((h::t)::rst)) ⇒
         ∀v. P v: thm

Next we define a curried recursive function, which uses wildcard expansion and pattern-matching pre-processing.

> Definition min_def: (min (SUC x) (SUC y) = min x y + 1)
                 /\   (min  ____    ____   = 0)
  End
<<HOL message: mk_functional: 
  pattern completion has added 1 clause to the original specification.>>
Equations stored under "min_def".
Induction stored under "min_ind".
val min_def =
   ⊢ (∀y x. min (SUC x) (SUC y) = min x y + 1) ∧ (∀v1. min 0 v1 = 0) ∧
     ∀v3. min (SUC v3) 0 = 0: thm

Next we make a primitive recursive definition. Note that no induction theorem is generated in this case.

> Definition filter:
    (filter P [] = [])
    /\    (filter P (h::t) = if P h then h::filter P t
                             else filter P t)
  End
<<HOL message: inventing new type variable names: 'a>>
Definition has been stored under "filter"
val filter =
   ⊢ (∀P. filter P [] = []) ∧
     ∀P h t. filter P (h::t) = if P h then h::filter P t else filter P t: thm

Definition may also be used to define mutually recursive functions. For example, we can define a datatype of propositions and a function for putting a proposition into negation normal form as follows. First we define a datatype, named prop, of boolean formulas:

> Datatype:
    prop = VARp 'a | NOTp prop | ANDp prop prop
         | ORp  prop prop
  End
<<HOL message: Defined type: "prop">>

Then two mutually recursive functions nnfpos and nnfneg are defined:

> Definition nnfpos_def:
     (nnfpos (VARp x)    = VARp x)
  /\ (nnfpos (NOTp p)    = nnfneg p)
  /\ (nnfpos (ANDp p q)  = ANDp (nnfpos p) (nnfpos q))
  /\ (nnfpos (ORp  p q)  = ORp  (nnfpos p) (nnfpos q))
  
  /\ (nnfneg (VARp x)    = NOTp (VARp x))
  /\ (nnfneg (NOTp p)    = nnfpos p)
  /\ (nnfneg (ANDp p q)  = ORp  (nnfneg p) (nnfneg q))
  /\ (nnfneg (ORp  p q)  = ANDp (nnfneg p) (nnfneg q))
  End
<<HOL message: inventing new type variable names: 'a>>
Equations stored under "nnfpos_def".
Induction stored under "nnfpos_ind".
val nnfpos_def =
   ⊢ (∀x. nnfpos (VARp x) = VARp x) ∧ (∀p. nnfpos (NOTp p) = nnfneg p) ∧
     (∀q p. nnfpos (ANDp p q) = ANDp (nnfpos p) (nnfpos q)) ∧
     (∀q p. nnfpos (ORp p q) = ORp (nnfpos p) (nnfpos q)) ∧
     (∀x. nnfneg (VARp x) = NOTp (VARp x)) ∧
     (∀p. nnfneg (NOTp p) = nnfpos p) ∧
     (∀q p. nnfneg (ANDp p q) = ORp (nnfneg p) (nnfneg q)) ∧
     ∀q p. nnfneg (ORp p q) = ANDp (nnfneg p) (nnfneg q): thm

Definition may also be used to define non-recursive functions:

> Definition fdef: fdef x (y,z) = (x + 1 = y DIV z)
  End
Definition has been stored under "fdef"
val fdef = ⊢ ∀x y z. fdef x (y,z) ⇔ x + 1 = y DIV z: thm

Finally, Definition may also be used to define non-recursive functions with complex pattern-matching. The pattern-matching pre-processing of Definition can be convenient for this purpose, but can also generate a large number of equations. It may also return theorems that do not look quite like what was originally input. For more on this, see the discussion of case expressions (into which the clauses in a Definition are converted internally) in Section 7.5.

When termination is not automatically proved

If the termination proof for a prospective definition fails, the invocation of the definitional machinery fails. In such situations, the user must supply a termination argument explicitly. The primitive machinery for beginning this operation is the ML function Hol_defn:

Hol_defn : string -> term quotation -> Defn.defn

Hol_defn makes the requested definition, but defers the proof of termination to the user. For setting up termination proofs, there are several useful entrypoints, namely

Defn.tgoal  : Defn.defn -> GoalstackPure.proofs
Defn.tprove : Defn.defn * tactic -> thm * thm

Defn.tgoal is analogous to set_goal and Defn.tprove is analogous to prove. Thus, Defn.tgoal is used to take the result of Hol_defn and set up a goal for proving termination of the definition.

Example

An invocation of Define on the following equations for Quicksort will currently fail, since the termination proof is currently beyond the capabilities of the naive termination prover. Instead, we make an application of Hol_defn:

> val qsort_defn =
    Hol_defn "qsort"
      `(qsort ord [] = []) /\
       (qsort ord (h::t) =
           qsort ord (FILTER (\x. ord x h) t)
           ++ [h] ++
           qsort ord (FILTER (\x. ~(ord x h)) t))`;
<<HOL message: inventing new type variable names: 'a>>
val qsort_defn =
   HOL function definition (recursive)
   
   Equation(s) :
    [...] ⊢ qsort ord [] = []
    [...]
   ⊢ qsort ord (h::t) =
     qsort ord (FILTER (λx. ord x h) t) ⧺ [h] ⧺
     qsort ord (FILTER (λx. ¬ord x h) t)
   
   Induction :
    [...]
   ⊢ ∀P. (∀ord. P ord []) ∧
         (∀ord h t.
            P ord (FILTER (λx. ¬ord x h) t) ∧ P ord (FILTER (λx. ord x h) t) ⇒
            P ord (h::t)) ⇒
         ∀v v1. P v v1
   
   Termination conditions :
     0. ∀t h ord. R (ord,FILTER (λx. ¬ord x h) t) (ord,h::t)
     1. ∀t h ord. R (ord,FILTER (λx. ord x h) t) (ord,h::t)
     2. WF R: DefnBase.defn

which returns a value of type defn, but does not try to prove termination.

The type defn has a prettyprinter installed for it: the above output is typical, showing the components of a defn in an understandable format. Although it is possible to directly work with elements of type defn, it is more convenient to invoke Defn.tgoal, which sets up a termination proof in a goalstack.

> Defn.tgoal qsort_defn;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        ∃R. WF R ∧ (∀t h ord. R (ord,FILTER (λx. ¬ord x h) t) (ord,h::t)) ∧
            ∀t h ord. R (ord,FILTER (λx. ord x h) t) (ord,h::t)

The goal is to find a wellfounded relation on the arguments to qsort and show that the arguments to qsort are in the relation. The function WF_REL_TAC is almost invariably used at this point to initiate the termination proof. Clearly, qsort terminates because the list argument gets shorter. Invoking WF_REL_TAC with the appropriate measure function results in two subgoals, both of which are easy to prove.

> e (WF_REL_TAC `measure (LENGTH o SND)`);
OK..
1 subgoal:
val it =
   
   (∀t h ord. LENGTH (FILTER (λx. ¬ord x h) t) < LENGTH (h::t)) ∧
   ∀t h ord. LENGTH (FILTER (λx. ord x h) t) < LENGTH (h::t)

Execution of WF_REL_TAC has automatically proved the wellfoundedness of the termination relation measure (LENGTH o SND) and the remainder of the goal has been simplified into a pair of easy goals. Once both goals are proved, we can encapsulate the termination proof/tactic into the Termination section of a Definition. As long as the tactic does indeed prove the termination conditions, the recursion equations and induction theorem are stored in the current theory segment before the recursion equations are returned:

> Definition qsort_def:
    (qsort ord [] = []) /\
    (qsort ord (h::t) =
         qsort ord (FILTER (\x. ord x h) t) ++ [h] ++
         qsort ord (FILTER (\x. ~(ord x h)) t))
  Termination
    WF_REL_TAC `measure (LENGTH o SND)` THEN cheat
  End
<<HOL message: inventing new type variable names: 'a>>
Equations stored under "qsort_def".
Induction stored under "qsort_ind".
val qsort_def =
   ⊢ (∀ord. qsort ord [] = []) ∧
     ∀t ord h.
       qsort ord (h::t) =
       qsort ord (FILTER (λx. ord x h) t) ⧺ [h] ⧺
       qsort ord (FILTER (λx. ¬ord x h) t): thm

As we have not specified a special name for it, the custom induction theorem for our function can be obtained under the name qsort_ind, or by using fetch, which returns named elements in the specified theory.2

> DB.fetch "-" "qsort_ind";
val it =
   ⊢ ∀P. (∀ord. P ord []) ∧
         (∀ord h t.
            P ord (FILTER (λx. ¬ord x h) t) ∧ P ord (FILTER (λx. ord x h) t) ⇒
            P ord (h::t)) ⇒
         ∀v v1. P v v1: thm

The induction theorem produced by Definition can be applied by recInduct. See the bossLib section of the Libraries chapter for details.

Techniques for proving termination

There are two problems to deal with when trying to prove termination. First, one has to understand, intuitively and then mathematically, why the function under consideration terminates. Second, one must be able to phrase this in HOL. In the following, we shall give a few examples of how this is done.

There are a number of basic and advanced means of specifying wellfounded relations. The most common starting point for dealing with termination problems for recursive functions is to find some function, known as a measure under which the arguments of a function call are larger than the arguments to any recursive calls that result.

For a very simple starter example, consider the following definition of a function that computes the greatest common divisor of two numbers:

> val gcd_defn =
     Hol_defn "gcd"
        `(gcd (0,n) = n) /\
         (gcd (m,n) = gcd (n MOD m, m))`;
val gcd_defn =
   HOL function definition (recursive)
   
   Equation(s) :
    [..] ⊢ gcd (0,n) = n
    [..] ⊢ gcd (SUC v2,n) = gcd (n MOD SUC v2,SUC v2)
   
   Induction :
    [..]
   ⊢ ∀P. (∀n. P (0,n)) ∧ (∀v2 n. P (n MOD SUC v2,SUC v2) ⇒ P (SUC v2,n)) ⇒
         ∀v v1. P (v,v1)
   
   Termination conditions :
     0. ∀v2 n. R (n MOD SUC v2,SUC v2) (SUC v2,n)
     1. WF R: DefnBase.defn

> Defn.tgoal gcd_defn;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        ∃R. WF R ∧ ∀v2 n. R (n MOD SUC v2,SUC v2) (SUC v2,n)

The invocation gcd(m,n) recurses in its first argument, and since we know that m is not 0, it is the case that n MOD m is smaller than m. The way to phrase the termination of gcd in HOL is to use a measure function to map from the domain of gcd — a pair of numbers — to a number. The definition of measure in HOL is equivalent to

prim_recTheory.measure_thm
  ⊢ ∀f x y. measure f x y ⇔ f x < f y

Now we must pick out the argument position to measure and invoke WF_REL_TAC:

> e (WF_REL_TAC ‘measure FST’);
OK..
val it =
   Initial goal proved.
   ⊢ (gcd (0,n) = n ∧ gcd (SUC v2,n) = gcd (n MOD SUC v2,SUC v2)) ∧
     ∀P. (∀n. P (0,n)) ∧ (∀v2 n. P (n MOD SUC v2,SUC v2) ⇒ P (SUC v2,n)) ⇒
         ∀v v1. P (v,v1): proof

The built-in reasoning then suffices to prove the remaining goal.

Weighting functions

Sometimes one needs a measure function that is itself recursive. For example, consider a type of binary trees and a function that linearizes trees. The algorithm works by rotating the tree until it gets a LeafB in the left branch, then it recurses into the right branch. At the end of execution the tree has been linearized.

> Datatype: btreeB = LeafB | BrhB btreeB btreeB
  End   ... output elided ...

> val Unbal_defn =
     Hol_defn "Unbal"
       `(Unbal LeafB = LeafB)
    /\  (Unbal (BrhB LeafB bt) = BrhB LeafB (Unbal bt))
    /\  (Unbal (BrhB (BrhB bt1 bt2) bt) = Unbal (BrhB bt1 (BrhB bt2 bt)))`;
val Unbal_defn =
   HOL function definition (recursive)
   
   Equation(s) :
    [...] ⊢ Unbal LeafB = LeafB
    [...] ⊢ Unbal (BrhB LeafB bt) = BrhB LeafB (Unbal bt)
    [...] ⊢ Unbal (BrhB (BrhB bt1 bt2) bt) = Unbal (BrhB bt1 (BrhB bt2 bt))
   
   Induction :
    [...]
   ⊢ ∀P. P LeafB ∧ (∀bt. P bt ⇒ P (BrhB LeafB bt)) ∧
         (∀bt1 bt2 bt.
            P (BrhB bt1 (BrhB bt2 bt)) ⇒ P (BrhB (BrhB bt1 bt2) bt)) ⇒
         ∀v. P v
   
   Termination conditions :
     0. ∀bt bt2 bt1. R (BrhB bt1 (BrhB bt2 bt)) (BrhB (BrhB bt1 bt2) bt)
     1. ∀bt. R bt (BrhB LeafB bt)
     2. WF R: DefnBase.defn

The termination conditions above can be turned into an interactive goal with Defn.tgoal:

> Defn.tgoal Unbal_defn;
val it =
   Proof manager status: 1 proof.
   1. Incomplete goalstack:
        Initial goal:
        ∃R. WF R ∧
            (∀bt bt2 bt1. R (BrhB bt1 (BrhB bt2 bt)) (BrhB (BrhB bt1 bt2) bt)) ∧
            ∀bt. R bt (BrhB LeafB bt)

Since the size of the tree is unchanged in the last clause in the definition of Unbal, a simple size measure will not work. Instead, we can assign weights to nodes in the tree such that the recursive calls of Unbal decrease the total weight in every case. One such assignment is

> Definition Weight_def:
    (Weight (LeafB) = 0) /\
    (Weight (BrhB x y) = (2 * Weight x) + (Weight y) + 1)
  End
Definition has been stored under "Weight_def"
val Weight_def =
   ⊢ Weight LeafB = 0 ∧ ∀x y. Weight (BrhB x y) = 2 * Weight x + Weight y + 1:
   thm

Now we can invoke WF_REL_TAC:

> e (WF_REL_TAC `measure Weight`);
OK..
1 subgoal:
val it =
   
   (∀bt bt2 bt1.
      Weight (BrhB bt1 (BrhB bt2 bt)) < Weight (BrhB (BrhB bt1 bt2) bt)) ∧
   ∀bt. Weight bt < Weight (BrhB LeafB bt)

Both conjuncts of this goal are quite easy to prove.

The technique of “weighting” nodes in a datatype in order to prove termination also goes by the name of polynomial interpretation. It must be admitted that finding the correct weighting for a termination proof is more an art than a science. Typically, one makes a guess and then tries the termination proof to see if it works.

Lexicographic combinations

Occasionally, there's a combination of factors that complicate the termination argument. For example, the following specification describes a naive pattern matching algorithm on strings (represented as lists here). The function takes four arguments: the first, $p$, is the remainder of the pattern being matched. The second, $\mathit{rst}$, is the remainder of the string being searched. The third argument, $p_0$, holds the original pattern to be matched. The fourth argument, $s$, is the string being searched.

> val match_defn =
     Hol_defn "match"
       `(match [] __ __ __ = T)  /\
        (match __ [] __ __ = F)  /\
        (match (a::pp) (b::ss) p0 s =
          if a=b then match pp ss p0 s
            else
          if NULL(s) then F
            else
          match p0 (TL s) p0 (TL s))`;   ... output elided ...

> Definition Match_def:  Match pat str = match pat str pat str
  End
<<HOL message: inventing new type variable names: 'a>>
Definition has been stored under "Match_def"
val Match_def = ⊢ ∀pat str. Match pat str ⇔ match pat str pat str: thm

The first clause of the definition states that if $p$ becomes exhausted, then a match has been found; the function returns T. The second clause represents the case where $s$ becomes exhausted but $p$ is not, in which case the function returns F. The remaining case is when there's more searching to do; the function checks if the head of the pattern $p$ is the same as the head of $\mathit{rst}$. If yes, then the search proceeds recursively, using the tail of $p$ and the tail of $\mathit{rst}$. If no, that means that $p$ has failed to match, so the algorithm advances one character ahead in $s$ and starts matching from the beginning of $p_0$. If $s$ is empty, however, then we return F. Note that $\mathit{rst}$ and $s$ both represent the string being searched: $\mathit{rst}$ is a “local” version of $s$: we recurse into $\mathit{rst}$ as long as there are matches with the pattern $p$. However, if the search eventually fails, then $s$, which “remembers” where the search started from, is used to restart the search.

So much for the behaviour of the function. Why does it terminate? There are two recursive calls. The first call reduces the size of $p$ and $\mathit{rst}$, and leaves the other arguments unchanged. The second call can increase the size of $p$ and $\mathit{rst}$, but reduces the size $s$. This is a classic situation in which to use a lexicographic ordering: some arguments to the function are reduced in some recursive calls, and some others are reduced in other recursive calls. Recall that LEX is an infix operator, defined in pairTheory as follows:

pairTheory.LEX_DEF
  ⊢ ∀R1 R2. R1 LEX R2 = (λ(s,t) (u,v). R1 s u ∨ s = u ∧ R2 t v)

In the second recursive call, the length of s is reduced, and in the first it stays the same. This motivates having the length of the $s$ be the first component of the lexicographic combination, and the length of $\mathit{rst}$ as the second component. Formally, we want to map from the four-tuple of arguments into a lexicographic combination of relations. This is enabled by inv_image from relationTheory:

relationTheory.inv_image_def
  ⊢ ∀R f. inv_image R f = (λx y. R (f x) (f y))

The desired relation maps from the four-tuple of arguments into a pair of numbers $(m,n)$, where $m$ is the length of the fourth argument, and $n$ is the length of the second argument. These lengths are then compared lexicographically with respect to less-than ($<$).

> Defn.tgoal match_defn;   ... output elided ...

> e (WF_REL_TAC `inv_image($< LEX $<) (\(w,x,y,z). (LENGTH z,LENGTH x))`);
OK..
1 subgoal:
val it =
   
   (∀ss s b a.
      a ≠ b ∧ ¬NULL s ⇒
      LENGTH (TL s) < LENGTH s ∨
      LENGTH (TL s) = LENGTH s ∧ LENGTH (TL s) < LENGTH (b::ss)) ∧
   ∀ss a. LENGTH ss < LENGTH (a::ss)

The first conjunct needs a case-split on s before it is proved by rewriting, and the second is also easy to prove by rewriting.

How termination conditions are synthesized

It is occasionally important to understand, at least in part, how Hol_defn constructs termination constraints. In some cases, it is even necessary for users to influence this process in order to have correct termination constraints extracted. The process is driven by so-called congruence theorems for particular HOL constants. For example, consider the following recursive definition of factorial:

  fact n = if n=0 then 1 else n * fact (n-1)

In the absence of knowledge of how the “if-then-else” construct affects the context of recursive calls, Hol_defn would extract the termination constraints:

  0. WF R
  1. !n. R (n - 1) n

which are unprovable, because the context of the recursive call has not been taken account of. This example is in fact not a problem for HOL, since the following congruence theorem is known to Hol_defn:

 |- !b b' x x' y y'.
      (b = b') /\
      (b' ==> (x = x')) /\
      (~b' ==> (y = y')) ==>
       ((if b then x else y) = (if b' then x' else y'))

This theorem is understood by Hol_defn as an ordered sequence of instructions to follow when the termination condition extractor hits an “if-then-else”. The theorem is read as follows: when an instance "if $B$then$X$else$Y$" is encountered while the extractor traverses the function definition, do the following:

  1. Traverse $B$ and extract termination conditions $\mathit{TCs}(B)$ from any recursive calls in it. This returns a theorem $\mathit{TCs}(B) \vdash B = B'$.

  2. Assume $B'$ and extract termination conditions from any recursive calls in $X$. This returns a theorem $\mathit{TCs}(X) \vdash X = X'$.

  3. Assume $\neg B'$ and extract termination conditions from any recursive calls in $Y$. This returns a theorem $\mathit{TCs}(Y) \vdash Y = Y'$.

  4. By equality reasoning with (1), (2), and (3), derive the theorem $$\mathit{TCs}(B) \cup \mathit{TCs}(X) \cup \mathit{TCs}(Y) \vdash (\mathtt{if}\ B\ \mathtt{then}\ X\ \mathtt{else}\ Y) = (\mathtt{if}\ B'\ \mathtt{then}\ X'\ \mathtt{else}\ Y')$$

  5. Replace "if $B$then$X$else$Y$" by "if $B'$then$X'$else$Y'$".

The termination conditions are accumulated until the extraction process finishes, and appear as hypotheses in the final result. Thus the extracted termination conditions for fact are

   0. WF R
   1. !n. ~(n = 0) ==> R (n - 1) n

and are easy to prove. The notion of context of a recursive call is defined by the set of congruence rules used in extracting termination conditions. This set can be obtained by invoking DefnBase.read_congs, and manipulated by DefnBase.add_cong, DefnBase.drop_cong and DefnBase.export_cong. The “add” and “drop” functions only affect the current state of the congruence database; in contrast, the “export” function provides a way for theories to specify that a particular theorem should be added to the congruence database in all descendant theories.

Higher-order recursion and congruence rules

A “higher-order” recursion is one in which a higher-order function is used to apply the recursive function to arguments. In order for the correct termination conditions to be proved for such a recursion, congruence rules for the higher order function must be known to the termination condition extraction mechanism. Congruence rules for common higher-order functions, e.g., MAP, EVERY, and EXISTS for lists, are already known to the mechanism. However, at times, one must manually prove and install a congruence theorem for a new user-defined higher-order function.

For example, suppose we define a higher-order function SIGMA for summing the results of a function in a list.

> Definition SIGMA_def:
   (SIGMA f [] = 0) /\
   (SIGMA f (h::t) = f h + SIGMA f t)
  End   ... output elided ...

We then use SIGMA in the definition of a function for summing the results of a function in a arbitrarily (finitely) branching tree.

> Datatype: ltree = LNode 'a (ltree list)
  End   ... output elided ...
  Defn.Hol_defn
    "ltree_sigma"
    `ltree_sigma f (LNode v tl) = f v + SIGMA (ltree_sigma f) tl`;

In this definition, SIGMA is applied to a partial application (ltree_sigma f) of the function being defined. Such a situation is called a higher-order recursion. Since the recursive call of ltree_sigma is not fully applied, special efforts have to be made to extract the correct termination conditions. Otherwise, the following unhappy situation results:

<<HOL message: inventing new type variable names: 'a>>
val it =
   HOL function definition (recursive)
   
   Equation(s) :
    [..] ⊢ ltree_sigma f (LNode v tl) = f v + SIGMA (λa. ltree_sigma f a) tl
   
   Induction :
    [..] ⊢ ∀P. (∀f v tl. (∀a. P f a) ⇒ P f (LNode v tl)) ⇒ ∀v v1. P v v1
   
   Termination conditions :
     0. ∀tl v f a. R (f,a) (f,LNode v tl)
     1. WF R: defn

The termination conditions for ltree_sigma seem to require finding a wellfounded relation R such that the pair (f,a) is R-less than (f, LNode v tl). However, this is a hopeless task, since there is no relation between a and LNode v tl, besides the fact that they are both ltrees. The termination condition extractor has not performed properly, because it didn't know a congruence rule for SIGMA. Such a congruence theorem is the following:

SIGMA_CONG =
 |- !l1 l2 f g.
     (l1=l2) /\ (!x. MEM x l2 ==> (f x = g x)) ==>
     (SIGMA f l1 = SIGMA g l2)

Once Hol_defn has been told about this theorem, via DefnBase's add_cong or export_cong functions, or by using a cong attribute on a theorem when it is saved, the termination conditions extracted for the definition are now provable, since a is a proper subterm of LNode v tl.

> val _ = DefnBase.add_cong SIGMA_CONG;
> Defn.Hol_defn "ltree_sigma"
   `ltree_sigma f (LNode v tl) = f v + SIGMA (ltree_sigma f) tl`;
<<HOL message: inventing new type variable names: 'a>>
val it =
   HOL function definition (recursive)
   
   Equation(s) :
    [..] ⊢ ltree_sigma f (LNode v tl) = f v + SIGMA (λa. ltree_sigma f a) tl
   
   Induction :
    [..]
   ⊢ ∀P. (∀f v tl. (∀a. MEM a tl ⇒ P f a) ⇒ P f (LNode v tl)) ⇒ ∀v v1. P v v1
   
   Termination conditions :
     0. ∀v f tl a. MEM a tl ⇒ R (f,a) (f,LNode v tl)
     1. WF R: defn

Recursion schemas

In higher order logic, very general patterns of recursion, known as recursion schemas or sometimes program schemas, can be defined. One example is the following:

$$ \mathsf{linRec}(x) = \mathtt{if}\;d(x)\;\mathtt{then}\;e(x)\;\mathtt{else}\;f(\mathsf{linRec}(g\;x)) $$

In this specification, the variables $d$, $e$, $f$, and $g$ are functions, that, when instantiated in different ways, allow $\mathsf{linRec}$ to implement different recursive functions. In this, $\mathsf{linRec}$ is like many other higher order functions. However, notice that if $d(x) = \mathsf{F}$, $f(x) = x+1$, and $g(x) = x$, then the resulting instantiation of $\mathsf{linRec}$ could be used to obtain a contradiction:

$$ \mathsf{linRec}(x) = \mathsf{linRec}(x) + 1 $$

This is not, however, derivable in HOL, because recursion schemas are defined by instantiating the wellfounded recursion theorem, and therefore certain abstract termination constraints arise that must be satisfied before recursion equations can be used in an unfettered manner. The entrypoint for defining a schema is TotalDefn.DefineSchema, which can also be targeted by using the schematic attribute with the Definition syntax. On the $\mathsf{linRec}$ example it behaves as follows (note that the schematic variables should only occur on the right-hand side of the definition when making the definition of a schema):

> Definition linRec_def[schematic]:
    linRec (x:'a) = if d(x) then e(x) else f(linRec(g x))
  End
<<HOL message: inventing new type variable names: 'b>>
<<HOL message: Definition is schematic in the following variables:
    "d", "e", "f", "g">>
<<HOL message: Unable to add linRec_def to global compset>>
Equations stored under "linRec_def".
Induction stored under "linRec_ind".
val linRec_def =
    [..]
   ⊢ ∀x f e. linRec d e f g x = if d x then e x else f (linRec d e f g (g x)):
   thm

The hypotheses of the returned theorem hold the abstract termination constraints. A similarly constrained induction theorem is also stored in the current theory segment.

> hyp linRec_def;
val it = [“∀x. ¬d x ⇒ R (g x) x”, “WF R”]: term list

These constraints are abstract, since they place termination requirements on variables that have not yet been instantiated. Once instantiations for the variables are found, then the constraints may be eliminated by finding a suitable wellfounded relation for R and then proving the other constraints.

Inductive Relations

Inductive definitions are made with the special Inductive syntax form, which wraps the underlying function Hol_reln, which is in turn found in the bossLib structure. The resulting definitions and theorems are handled with functions defined in the library IndDefLib. The Inductive syntax and Hol_reln function take a term quotation as input and attempt to define the relations there specified. The input term quotation must parse to a term that conforms to the following grammar:

$$ \begin{aligned} \langle\mathit{inputFormat}\rangle & ::= \langle\mathit{clause}\rangle\;\land\; \langle\mathit{inputFormat}\rangle \;\mid\;\langle\mathit{clause}\rangle \\ \langle\mathit{clause}\rangle & ::= (\forall x_1 \dots x_n.\;\langle\mathit{hypothesis}\rangle \;\Rightarrow\;\langle\mathit{conclusion}\rangle) \\ & \;\mid\; (\forall x_1 \dots x_n.\;\langle\mathit{conclusion}\rangle) \\ \langle\mathit{conclusion}\rangle & ::= \langle\mathit{con}\rangle\;\mathit{sv}_1\;\mathit{sv}_2\dots \\ \langle\mathit{hypothesis}\rangle & ::= \text{any term} \\ \langle\mathit{con}\rangle & ::= \text{a new relation constant} \end{aligned} $$

The (optional) $\mathit{sv}_i$ terms that appear after a constant name are so-called “schematic variables”. The same variables must always follow all new constants throughout the definition. These variables and the names of the constants-to-be must not be quantified over in each $\langle\mathit{clause}\rangle$. A $\langle\mathit{clause}\rangle$ should have no other free variables. Any that occur will be universally quantified as part of the process of definition, and a warning message emitted. (Universal quantifiers at the head of the clause can be used to bind free variables, but it is also permissible to use existential quantification in the hypotheses. If a clause has no free variables, it is permissible to have no universal quantification.)

A successful invocation of this definitional principle returns three important theorems $\mathit{rules}$, $\mathit{ind}$ and $\mathit{cases}$. Each is also stored in the current theory segment.

  • $\mathit{rules}$ is a conjunction of implications that will be the same as the input term quotation; the theorem is saved under the name <stem>_rules, where <stem> is the name of the first relation defined by the function (if using Hol_reln), or as provided by the user, when using the Inductive syntax.

  • $\mathit{ind}$ is the induction principle for the relations, saved under the name <stem>_ind.

  • $\mathit{cases}$ is the so-called “cases” or “inversion” theorem for the relations, saved under the name <stem>_cases. A cases theorem is of the form

       (!a0 .. an.  R1 a0 .. an = <R1's first rule possibility> \/
                                  <R1's second rule possibility> \/ ...)
                       /\
       (!a0 .. am.  R2 a0 .. am = <R2's first rule possibility> \/
                                  <R2's second rule possibility> \/ ...)
                       /\
       ...
    

    and is used to decompose an element in the relation into the possible ways of obtaining it by the rules.

If the “stem” of the first constant defined in a set of clauses is such that resulting ML bindings in an exported theory file will result in illegal ML, then the xHol_reln function should be used. The xHol_reln function is analogous to the xDefine function for defining recursive functions (see Section 7.6).

Alternatively, the Inductive syntax can be used, requiring the user to specify the stem, but saving on verbosity: instead of writing

   val (foo_rules,foo_ind,foo_cases) = Hol_reln`
     ...
   `;

one writes

   Inductive foo:
     ...
   End

where, as with other special syntaxes, the keywords (Inductive and End) have to be in the leftmost column of the source file. Additionally, users can automatically export each rule as a theorem by assigning a name using the square-bracket syntax. The appropriate format is [~Name:], where Name acts as the placeholder for the rule name. Inside the square brackets, the tilde (~) is optional; if included, it prefixes the exported rule name with <stem>_. For instance, the following example will export rules as rule1 and foo_rule2.

   Inductive foo:
   [rule1:]
     ...
   [~rule2:]
     ...
   ...
   End

Strong induction principles

So called “strong” versions of induction principles (in which instances of the relation being defined appear as extra hypotheses), are automatically proved when an inductive definition is made. The strong induction principle for a relation is used when the Induct_on tactic is used.

Adding monotone operators

New constants may occur recursively throughout rules' hypotheses, as long as it can be shown that the rules remain monotone with respect to the new constants. Hol_reln automatically attempts to prove such monotonicity results, using a set of theorems held in a reference IndDefLib.the_monoset. Monotonicity theorems must be of the form

$$ \mathit{cond}_1 \land \cdots \land \mathit{cond}_m \Rightarrow (\mathit{Op}\;\mathit{arg}_1 \dots \mathit{arg}_n \Rightarrow \mathit{Op}\;\mathit{arg}'_1 \dots \mathit{arg}'_n) $$

where each $\mathit{arg}$ and $\mathit{arg}'$ term must be a variable, and where there must be as many $\mathit{cond}_i$ terms as there are arguments to $\mathit{Op}$ that vary. Each $\mathit{cond}_i$ must be of the form

$$ \forall \vec{v}.\;\mathit{arg}\;\vec{v} \Rightarrow \mathit{arg}'\;\vec{v} $$

where the vector of variables $\vec{v}$ may be empty, and where the $\mathit{arg}$ and $\mathit{arg}'$ may actually be reversed (as in the rule for negation).

For example, the monotonicity rule for conjunction is

$$ (P \Rightarrow P') \land (Q \Rightarrow Q') \Rightarrow (P \land Q \Rightarrow P' \land Q') $$

The monotonicity rule for the EVERY operator in the theory of lists (see Section 5.4.1), is

$$ (\forall x.\;P(x) \Rightarrow Q(x)) \Rightarrow (\mathtt{EVERY}\;P\;\ell \Rightarrow \mathtt{EVERY}\;Q\;\ell) $$

With a monotonicity result available for an operator such as EVERY, it is then possible to write inductive definitions where hypotheses include mention of the new relation as arguments to the given operators.

Monotonicity results that the user derives may be stored in the global the_monoset variable by using the export_mono function, or the mono theorem attribute. This function takes a string naming a theorem in the current theory segment, and adds that theorem to the monotonicity theorems immediately, and in such a way that this situation will also obtain when the current theory is subsequently reloaded.

Examples

A simple example of defining two mutually recursive relations is the following:

> Inductive EVEN_ODD:
    EVEN 0 /\
    (!n. ODD n ==> EVEN (n + 1)) /\
    (!n. EVEN n ==> ODD (n + 1))
  End
val EVEN_ODD_cases =
   ⊢ (∀a0. EVEN a0 ⇔ a0 = 0 ∨ ∃n. a0 = n + 1 ∧ ODD n) ∧
     ∀a1. ODD a1 ⇔ ∃n. a1 = n + 1 ∧ EVEN n: thm
val EVEN_ODD_ind =
   ⊢ ∀EVEN' ODD'.
       EVEN' 0 ∧ (∀n. ODD' n ⇒ EVEN' (n + 1)) ∧ (∀n. EVEN' n ⇒ ODD' (n + 1)) ⇒
       (∀a0. EVEN a0 ⇒ EVEN' a0) ∧ ∀a1. ODD a1 ⇒ ODD' a1: thm
val EVEN_ODD_rules =
   ⊢ EVEN 0 ∧ (∀n. ODD n ⇒ EVEN (n + 1)) ∧ ∀n. EVEN n ⇒ ODD (n + 1): thm

The next example shows how to inductively define the reflexive and transitive closure of relation $R$, which we write as rtc. Note that R, as a schematic variable, is not quantified in the rules. This is appropriate because it is rtc R that has the inductive characterisation, not rtc itself.

> Inductive rtc:
    (!x. rtc R x x) /\
    (!x z. (?y. R x y /\ rtc R y z) ==> rtc R x z)
  End
<<HOL message: inventing new type variable names: 'a>>
<<HOL message: Treating "R" as schematic variable>>
val rtc_cases = ⊢ ∀R a0 a1. rtc R a0 a1 ⇔ a1 = a0 ∨ ∃y. R a0 y ∧ rtc R y a1:
   thm
val rtc_ind =
   ⊢ ∀R rtc'.
       (∀x. rtc' x x) ∧ (∀x z. (∃y. R x y ∧ rtc' y z) ⇒ rtc' x z) ⇒
       ∀a0 a1. rtc R a0 a1 ⇒ rtc' a0 a1: thm
val rtc_rules =
   ⊢ ∀R. (∀x. rtc R x x) ∧ ∀x z. (∃y. R x y ∧ rtc R y z) ⇒ rtc R x z: thm

Inductive definitions may be used to define multiple relations, as in the definition of EVEN and ODD. The relations may or may not be mutually recursive. The clauses for each relation need not be contiguous.

Proofs with inductive relations

The “rules” theorem of an inductive relation provides a straightforward way of proving arguments belong to a relation. If confronted with a goal of the form R x y, one might make progress by performing a MATCH_MP_TAC (or perhaps, an HO_MATCH_MP_TAC) with one of the implications in the “rules” theorem.

The “cases” theorem can be used for the same purpose because it is an equality, of the general form R x y $\Leftrightarrow$ $\ldots$. Because the right-hand side of this theorem will often include other occurrences of the relation, it is generally not safe to simply rewrite with it. The rewriting-control directives Once, SimpLHS and SimpRHS can be useful here. In addition, the “cases” theorem can be used as an “elimination” form: if one has an assumption of the form R x y, rewriting this (perhaps with FULL_SIMP_TAC if the term occurs in the goal's assumptions) into the possible ways it may have come about is often a good approach.

Inductive relations naturally also support proof by induction. Because an inductive relation is the least relation satisfying the given rules, one can use induction to show goals of the form

   !x y. R x y ==> P

where P is an arbitrary predicate likely including references to variables x and y.

The low-level approach to goals of this form is to apply

   HO_MATCH_MP_TAC R_ind

A slightly more high-level approach is use the Induct_on tactic, which will actually use the automatically generated “strong” induction principle.3 (This tactic is also used to perform structural inductions over algebraic data types; see the bossLib section of the Libraries chapter.) When performing a rule induction, the quotation passed to Induct_on should be of the constant, perhaps also applied to arguments. Indeed, if there are multiple instances of an R-term in the goal, then quoting arguments allows selection of the correct term to induct on. Thus, one can write invocations such as

   Induct_on `R (f x) y`

Coinductive Relations

HOL supports coinductive relational definitions by the function Hol_coreln in the bossLib structure (or with the special CoInductive syntax). Hol_coreln shares the same input syntax with Hol_reln. A successful invocation of this definitional principle returns three important theorems $\mathit{rules}$, $\mathit{coind}$ and $\mathit{cases}$. Each is also stored in the current theory segment.

  • $\mathit{rules}$ is a conjunction of implications that will be the same as the input term quotation; the theorem is saved under the name <stem>_rules, where <stem> is the name of the first relation defined by the function (if using Hol_coreln), or as provided by the user, when using the CoInductive syntax.
  • $\mathit{coind}$ is the coinduction principle for the relations, saved under the name <stem>_coind.
  • $\mathit{cases}$ is the so-called “cases” or “inversion” theorem for the relations, saved under the name <stem>_cases, and is used to decompose an element in the relation into the possible ways of obtaining it by the rules.

Note that “schematic variables” are also supported by Hol_coreln, while there is no concept of “strong coinduction principles” here. HOL's core coalgebra types (llist, path and lbtree) are all based on coinductive relations. See Section 5.5.4 for a sample usage of coinductive relations on bisimulation.


  1. HOL also supports another syntax for datatype definition through the Hol_datatype entrypoint. For more details on this syntax, see Hol_datatype's entry in \REFERENCE.

  2. In a call to fetch, the first argument denotes a theory; the current theory may be specified by "-".

  3. To get an approximation of the Induct_on call, one might write something like HO_MATCH_MP_TAC R_strongind.

Libraries

A library is an abstraction intended to provide a higher level of organization for HOL applications. In general, a library can contain a collection of theories, proof procedures, and supporting material, such as documentation. Some libraries simply provide proof procedures, such as simpLib, while others provide theories and proof procedures, such as intLib. Libraries can include other libraries.

In the HOL system, libraries are typically represented by SML structures named following the convention that library x will be found in the SML structure xLib. Loading this structure should load all the relevant sub-components of the library and set whatever system parameters are suitable for use of the library.

When the HOL system is invoked in its normal configuration, several useful libraries are automatically loaded. The most basic HOL library is boolLib, which supports the definitions of the HOL logic, found in the theory bool, and provides a useful suite of definition and reasoning tools.

Another pervasively used library is found in the structure Parse (the reader can see that we are not strictly faithful to our convention about library naming). The parser library provides support for parsing and ‘pretty-printing’ of HOL types, terms, and theorems.

The boss library provides a basic collection of standard theories and high-level proof procedures, and serves as a standard platform on which to work. It is preloaded and opened when the HOL system starts up. It includes boolLib and Parse. Theories provided include pair, sum, option; the arithmetic theories num, prim_rec, arithmetic, and numeral; and list. Other libraries included in bossLib are goalstackLib, which provides a proof manager for tactic proofs; simpLib, which provides a variety of simplifiers; numLib, which provides a decision procedure for arithmetic; Datatype, which provides high-level support for defining algebraic datatypes; and tflLib, which provides support for defining recursive functions.

Parsing and Prettyprinting

Every type and term in HOL is ultimately built by application of the primitive (abstract) constructors for types and terms. However, in order to accommodate a wide variety of mathematical expression, HOL provides flexible infrastructure for parsing and prettyprinting types and terms through the Parse structure.

The term parser supports type inference, overloading, binders, and various fixity declaration (infix, prefix, postfix, and combinations). There are also flags for controlling the behaviour of the parser. Further, the structure of the parser is exposed so that new parsers can be quickly constructed to support user applications.

The parser is parameterized by grammars for types and terms. The behaviour of the parser and prettyprinter is therefore usually altered by grammar manipulations. These can be of two kinds: temporary or permanent. Temporary changes should be used in library implementations, or in script files for those changes that the user does not wish to have persist in theories descended from the current one. Permanent changes are appropriate for use in script-files, and will be in force in all descendant theories. Functions making temporary changes are signified by a leading temp_ in their names.

Parsing types

The language of types is a simple one. An abstract grammar for the language is presented in Figure 8.1.1. The actual grammar (with concrete values for the infix symbols and type operators) can be inspected using the function type_grammar.

$$ \begin{array}{lcl} \tau &::=& \tau \;\odot\; \tau \;\;|\;\; \mathit{vtype} \;\;|\;\; \mathit{tyop} \;\;|\;\; \mathtt{(} \;\mathit{tylist}\;\mathtt{)} \;\mathit{tyop} \;\;|\;\; \tau \;\mathit{tyop} \;\;|\;\; \mathtt{(}\;\tau\;\mathtt{)} \;\;|\;\; \tau\mathtt{[}\tau\mathtt{]}\\ \odot &::=& \mathtt{->} \;\;|\;\; \mathtt{\#} \;\;|\;\; \mathtt{+} \;\;|\;\; \cdots\\ \mathit{vtype} &::=& \mathtt{'a} \;\;|\;\; \mathtt{'b} \;\;|\;\; \mathtt{'c} \;\;|\;\; \cdots\\ \mathit{tylist} &::=& \tau \;\;|\;\; \tau \;\mathtt{,}\;\mathit{tylist}\\ \mathit{tyop} &::=& \mathtt{bool} \;\;|\;\; \mathtt{list} \;\;|\;\; \mathtt{num} \;\;|\;\; \mathtt{fun} \;\;|\;\; \cdots \end{array} $$

Figure 8.1.1. An abstract grammar for HOL types ($\tau$). Infixes ($\odot$) always bind more weakly than type operators ($\mathit{tyop}$) (and type-subscripting $\tau\mathtt{[}\tau\mathtt{]}$), so that $\tau_1 \;\odot\; \tau_2\;\mathit{tyop}$ is always parsed as $\tau_1 \;\odot\; (\tau_2\;\mathit{tyop})$. Different infixes can have different priorities, and infixes at different priority levels can associate differently (to the left, to the right, or not at all). Users can extend the categories $\odot$ and $\mathit{tyop}$ by making new type definitions, and by directly manipulating the grammar.

Type infixes

Infixes may be introduced with the function add_infix_type. This sets up a mapping from an infix symbol (such as ->) to the name of an existing type operator (such as fun). The binary symbol needs to be given a precedence level and an associativity. See REFERENCE for more details.

Type abbreviations

Users can abbreviate common type patterns with abbreviations. This is done with the SML function type_abbrev:

   type_abbrev : string * hol_type -> unit

An abbreviation is a new type operator, of any number of arguments, that expands into an existing type. For example, one might develop a light-weight theory of numbers extended with an infinity, where the representing type was num option (NONE would represent the infinity value). One might set up an abbreviation infnum that expanded to this underlying type. Polymorphic patterns are supported as well. For example, as described in Section 5.5.1, the abbreviation set, of one argument, is such that :'a set expands into the type :'a -> bool, for any type :'a.

When types come to be printed, the expansion of abbreviations done by the parser is reversed if the type_abbrev_pp entry-point is used; otherwise the abbreviation is for input only. For more information, see type_abbrev's entry in REFERENCE.

There are special syntactic forms available for both type abbreviation entry-points. Instead of

   val _ = type_abbrev("set", “:'a -> bool”)

one can write

   Type set = “:'a -> bool”

and if an underlying call to type_abbrev_pp is desired, the [pp] “attribute” should be added to the name, thus:

   Type set[pp] = “:'a -> bool”

If a “temporary” abbreviation is required (one whose effect will not be apparent in descendent theories), then the local attribute can also be added to the Type syntax, or the SML functions temp_type_abbrev and temp_type_abbrev_pp can be used.

Parsing terms

The term parser provides a grammar-based infrastructure for supporting concrete syntax for formalizations. Usually, the HOL grammar gets extended when a new definition or constant specification is made. (The introduction of new constants is discussed in Sections 1.6.3.1 and 1.6.3.2.) However, any identifier can have a parsing status attached at any time. In the following, we explore some of the capabilities of the HOL term parser.

Parser architecture

The parser turns strings into terms. It does this in the following series of phases, all of which are influenced by the provided grammar. Usually this grammar is the default global grammar, but users can arrange to use different grammars if they desire. Strictly, parsing occurs after lexing has split the input into a series of tokens. For more on lexing, see Section 1.3.1.

Concrete Syntax: Features such as infixes, binders and mix-fix forms are translated away, creating an intermediate, “abstract syntax” form (ML type Absyn). The possible fixities are discussed in Section 8.1.2.6 below. Concrete syntax forms are added to the grammar with functions such as add_rule and set_fixity (for which, see the REFERENCE). The action of this phase of parsing is embodied in the function Absyn.

The Absyn data type is constructed using constructors AQ (an antiquote, see Section 8.1.3); IDENT (an identifier); QIDENT (a qualified identifier, given as thy$ident) ; APP (an application of one form to another); LAM (an abstraction of a variable over a body), and TYPED (a form accompanied by a type constraint1, see Section 8.1.2.4). At this stage of the translation, there is no distinction made between constants and variables: though QIDENT forms must be constants, users are also able to refer to constants by giving their bare names.

It is possible for names that occur in the Absyn value to be different from any of the tokens that appeared in the original input. For example, the input

   “if P then Q else R”

will turn into

   APP (APP (APP (IDENT "COND", IDENT "P"), IDENT "Q"), IDENT "R")

(This is slightly simplified output: the various constructors for Absyn, including APP, also take location parameters.)

The standard grammar includes a rule that associates the special mix-fix form for if-then-else expressions with the underlying “name” COND. It is COND that will eventually be resolved as the constant bool$COND.

If the “quotation” syntax with a bare dollar is used, then this phase of the parser will not treat strings as part of a special form. For example, “$if P” turns into the Absyn form

   APP(IDENT "if", IDENT "P")

not a form involving COND.

More typically, one often writes something like “$+ x”, which generates the abstract syntax

   APP(IDENT "+", IDENT "x")

Without the dollar-sign, the concrete syntax parser would complain about the fact that the infix plus did not have a left-hand argument. When the successful result of parsing is handed to the next phase, the fact that there is a constant called + will give the input its desired meaning.

Symbols can also be “escaped” by enclosing them in parentheses. Thus, the above could be written “(+) x” for the same effect.

The user can insert intermediate transformation functions of their own design into the parsing processing at this point. This is done with the function

   add_absyn_postprocessor

The user's function will be of type Absyn -> Absyn and can perform whatever changes are appropriate. Like all other aspects of parsing, these functions are part of a grammar: if the user doesn't want to see a particular function used, they can arrange for parsing to be done with respect to a different grammar.

Name Resolution: The bare IDENT forms in the Absyn value are resolved as free variables, bound names or constants. This process results in a value of the Preterm data type, which has similar constructors to those in Absyn except with forms for constants. A string can be converted straight to a Preterm by way of the Preterm function.

A bound name is the first argument to a LAM constructor, an identifier occurring on the left-hand side of a case-expression's arrow, or an identifier occurring within a set comprehension's pattern. A constant is a string that is present in the domain of the grammar's “overload map”. Free variables are all other identifiers. Free variables of the same name in a term will all have the same type. Identifiers are tested to see if they are bound, and then to see if they are constants. Thus it is possible to write

   \SUC. SUC + 3

and have the string SUC be treated as a number in the context of the given abstraction, rather than as the successor constant.

The “overload map” is a map from strings to lists of terms. The terms are usually just constants, but can be arbitrary terms (giving rise to “syntactic macros” or “patterns”). This facility is used to allow a name such as + to map to different addition constants in theories such as arithmetic, integer, and words. In this way the “real” names of the constants can be divorced from what the user types. In the case of addition, the natural number plus actually is called + (strictly, arithmetic$+); but over the integers, it is int_add, and over words it is word_add. (Note that because each constant is from a different theory and thus a different namespace, they could all have the name +.)

When name resolution determines that an identifier should be treated as a constant, it is mapped to a preterm form that lists all of the possibilities for that string. Subsequently, because the terms in the range of the overload map will typically have different types, type inference will often eliminate possibilities from the list. If multiple possibilities remain after type inference has been performed, then a warning will be printed, and one of the possibilities will be chosen. (Users can control which terms are picked when this situation arises.)

When a term in the overload map is chosen as the best option, it is substituted into the term at appropriate position. If the term is a lambda abstraction, then as many $\beta$-reductions are done as possible, using any arguments that the term has been applied to. It is in this way that a syntactic pattern can process arguments. (See also Section 8.1.2.3 for more on syntactic patterns.)

Type Inference: All terms in the HOL logic are well-typed. The kernel enforces this through the API for the term data type. (In particular, the mk_comb function checks that the type of the first argument is a function whose domain is equal to the type of the second argument.) The parser's job is to turn user-supplied strings into terms. For convenience, it is vital that the users do not have to provide types for all of the identifiers they type. (See Section 8.1.2.5 below.)

In the presence of overloaded identifiers, type inference may not be able to assign a unique type to all constants. If multiple possibilities exist, one will be picked when the Preterm is finally converted into a genuine term.

Conversion to Term: When a Preterm has been type-checked, the final conversion from that type to the term type is mostly straightforward. The user can insert further processing at this point as well, so that a user-supplied function modifies the result before the parser returns.

Unicode characters

It is possible to have the HOL parsing and printing infrastructure use Unicode characters (written in the UTF-8 encoding). This makes it possible to write and read terms such as

   ∀x. P x ∧ Q x

rather than

   !x. P x /\ Q x

If they wish, users may simply define constants that have Unicode characters in their names, and leave it at that. The problem with this approach is that standard tools will likely then create theory files that include (illegal) SML bindings like val →_def = .... The result will be ...Theory.sig and ...Theory.sml files that fail to compile, even though the call to export_theory may succeed. This problem can be finessed through the use of functions like set_MLname, but it's probably best practice to only use alphanumerics in the names of constants, and to then use functions like overload_on and add_rule to create Unicode syntax for the underlying constant.

If users have fonts with the appropriate repertoire of characters to display their syntax, and are confident that any other users of their theories will too, then this is perfectly reasonable. However, if users wish to retain some backwards compatibility a provide a pure ASCII syntax, they can do so by defining that pure ASCII syntax first. Having done this, they can create a Unicode version of the syntax with the function Unicode.unicode_version. Then, either Unicode or ASCII characters can be used to input the syntax, and, while the trace variable "PP.avoid_unicode" is $1$, the ASCII syntax will be used for printing. If the trace is set to $0$, then again, both syntaxes can be used to write the terms, but the pretty-printer will prefer the Unicode syntax when the terms are printed by the system.

For example, in boolScript.sml, the Unicode character for logical and (), is set up as a Unicode alternative for /\ with the call

   val _ = unicode_version {u = UChar.conj, tmnm = "/\\"};

(In this context, the Unicode structure has been open-ed, giving access also to the structure UChar which contains bindings for the Greek alphabet, and some common mathematical symbols.)

The argument to unicode_version is a record with fields u and tmnm. Both are strings. The tmnm field can either be the name of a constant, or a token appearing in a concrete syntax rule (possibly mapping to some other name). If the tmnm is only the name of a constant, then, with the trace variable enabled, the string u will be overloaded to the same name. If the tmnm is the same as a concrete syntax rule's token, then the behaviour is to create a new rule mapping to the same name, but with the string u used as the token.

Lexing rules with Unicode characters

Roughly speaking, HOL considers characters to be divided into three classes: alphanumerics, non-aggregating symbols and symbols. This affects the behaviour of the lexer when it encounters strings of characters. Unless there is a specific “mixed” token already in the grammar, tokens split when the character class changes. Thus, in the string

   ++a

the lexer will see two tokens, ++ and a, because + is a symbol and a is an alphanumeric. The classification of the additional Unicode characters is very simplistic: all Greek letters except λ are alphanumeric; the logical negation symbol ¬ is non-aggregating; a variety of parenthetical characters (e.g., $\llparenthesis\,\rrparenthesis$) are non-aggregating, and everything else is symbolic. (The exception for λ is to allow strings like λx.x to lex into four tokens.)

Overloading and Syntactic Patterns ("macros")

As earlier alluded to, a limited amount of overloading resolution is performed by the term parser. For example, the ‘tilde’ symbol (~) denotes boolean negation in the initial theory of HOL, and it also denotes the additive inverse in the integer and real theories. If we load the integer theory and enter an ambiguous term featuring ~, the system will inform us that overloading resolution is being performed.

> load "integerTheory";
val it = (): unit

> Term `~~x`;
<<HOL message: more than one resolution of overloading was possible>>
val it = “¬¬x”: term

> type_of it;
val it = “:bool”: hol_type

A priority mechanism is used to resolve multiple possible choices. In the example, ~ could be consistently chosen to have type :bool -> bool or :int -> int, and the mechanism has chosen the former. For finer control, explicit type constraints may be used. In the following session, the ~~x in the first quotation has type :bool, while in the second, a type constraint ensures that ~~x has type :int.

> Term `~(x = ~~x)`;
<<HOL message: more than one resolution of overloading was possible>>
val it = “(x :bool) ⇎ ¬¬x”: term

> Term `~(x:int = ~~x)`;
val it = “(x :int) ≠ --x”: term

Note that the symbol ~ stands for two different constants in the second quotation; its first occurrence is boolean negation, while the other two occurrences are the additive inverse operation for integers.

The prettiest way to introduce entries into the overload map is to use the Overload syntactic form:

   Overload name[attrs] = “term/pattern”

(The name can be a bare SML alpha-numeric identifier or enclosed in string-literal quotes; names that are “symbolic” in any way must be enclosed in string-literal double quotes. For example, Overload "-@-" = “...”.) Depending on the choice of attributes, this will map into a call to one of a variety of underlying “overload-on” functions. The default (without any attributes) calls overload_on, whose effect is to make an entry in the map that will be exported with the theory. The overloading effect thus persists in descendent theories. If the local attribute is used, this export doesn't occur, and the overloading effect is only visible for the rest of the containing script file.2

The other attribute that can be used is inferior, which causes the entry to be made, but makes it the pretty-printer's last choice when matching terms are printed.

All of this functionality ultimately stems from the “overload map” mentioned earlier. This is actually a combination of maps, one for parsing, and one for printing. The parsing map is from names to lists of terms, and determines how the names that appear in a Preterm will translate into terms. In essence, bound names turn into bound variables, unbound names not in the domain of the map turn into free variables, and unbound names in the domain of the map turn into one of the elements of the set associated with the given name. Each term in the set of possibilities may have a different type, so type inference will choose from those that have types consistent with the rest of the given term. If the resulting list contains more than one element, then the term appearing earlier in the list will be chosen.

The most common use-case for the overload map is have names map to constants. In this way, for example, the various numeric theories can map the string "+" to the relevant notions of addition, each of which is a different constant. However, the system has extra flexibility because names can map to arbitrary terms. For example, it is possible to map to specific type-instances of constants. Thus, the string "<=>" maps to equality, but where the arguments are forced to be of type ":bool".

Moreover, if the term mapped to is a lambda-abstraction (i.e., of the form $\lambda x.\;M$), then the parser will perform all possible $\beta$-reductions for that term and the arguments accompanying it. For example, in boolTheory and its descendants, the string "<>" is overloaded to the term “\x y. ~(x = y)”. Additionally, "<>" is set up at the concrete syntax level as an infix. When the user inputs “x <> y”, the resulting Absyn value is

   APP(APP(IDENT "<>", IDENT "x"), IDENT "x")

The "x" and "y" identifiers will map to free variables, but the "<>" identifier maps to a list containing “\x y. ~(x = y)”. This term has type

   :'a -> 'a -> bool

and the polymorphic variables are generalisable, allowing type inference to give appropriate (identical) types to x and y. Assuming that this option is the only overloading for "<>" left after type inference, then the resulting term will be ~(x = y). Better, though this will be the underlying structure of the term in memory, it will actually print as “x <> y”.

If the term mapped to in the overload map contains any free variables, these variables will not be instantiated in any way. In particular, if these variables have polymorphic types, then the type variables in those types will be constant: not subject to instantiation by type inference.

Pretty-printing and syntactic patterns

The second part of the “overload map” is a map from terms to strings, specifying how terms should be turned back into identifiers. (Though it does not actually construct an Absyn value, this process reverses the name resolution phase of parsing, producing something that is then printed according to the concrete syntax part of the given grammar.)

Because parsing can map single names to complicated term structures, printing must be able to take a complicated term structure back to a single name. It does this by performing term matching.3

If multiple patterns match the same term, then the printer picks the most specific match (the one that requires least instantiation of the pattern's variables). If this still results in multiple, equally specific, possibilities, the most recently added pattern takes precedence. (Users can thus manipulate the printer's preferences by making otherwise redundant Overload declarations.)

In the example of the not-equal-to operator above, the pattern will be ~(?x = ?y), where the question-marks indicate instantiable pattern variables. If a pattern includes free variables (recall that the x and y in this example were bound by an abstraction), then these will not be instantiable.

There is one further nicety in the use of this facility: “bigger” matches, covering more of a term, take precedence. The difficulty this can cause is illustrated in the IS_PREFIX pattern from rich_listTheory. For the sake of backwards compatibility this identifier maps to

   \x y. isPREFIX y x

where isPREFIX is a constant from listTheory. (The issue is that IS_PREFIX expects its arguments in reverse order to that expected by isPREFIX.) Now, when this macro is set up the overload map already contains a mapping from the string "isPREFIX" to the constant isPREFIX (this happens with every constant definition). But after the call establishing the new pattern for IS_PREFIX, the isPREFIX form will no longer be printed. Nor is it enough, to repeat the call

   Overload isPREFIX = “isPREFIX”

Instead (assuming that isPREFIX is indeed the preferred printing form), the call must be

   Overload isPREFIX = “\x y. isPREFIX x y”

so that isPREFIX's pattern is as long as IS_PREFIX's.

Type constraints

A term can be constrained to be of a certain type. For example, X:bool constrains the variable X to have type bool. An attempt to constrain a term inappropriately will raise an exception: for example,

   if T then (X:ind) else (Y:bool)

will fail because both branches of a conditional must be of the same type. Type constraints can be seen as a suffix that binds more tightly than everything except function application. Thus $\term\ \ldots\ \term : \type$ is equal to $(\term\ \ldots\ \term) : \type$, but $x < y:$num is a legitimate constraint on just the variable $y$.

The inclusion of : in the symbolic identifiers means that some constraints may need to be separated by white space. For example,

   $=:bool->bool->bool

will be broken up by the HOL lexer as

   $=: bool -> bool -> bool

and parsed as an application of the symbolic identifier $=: to the argument list of terms [bool, ->, bool, ->, bool]. A well-placed space will avoid this problem:

   $= :bool->bool->bool

is parsed as the symbolic identifier "=" constrained by a type. Instead of the $, one can also use parentheses to remove special parsing behaviour from lexemes:

   (=):bool->bool->bool

Type inference

Consider the term x = T: it (and all of its subterms) has a type in the HOL logic. Now, T has type bool. This means that the constant = has type xty -> bool -> bool, for some type xty. Since the type scheme for = is 'a -> 'a -> bool, we know that xty must in fact be bool in order for the type instance to be well-formed. Knowing this, we can deduce that the type of x must be bool.

Ignoring the jargon (“scheme” and “instance”) in the previous paragraph, we have conducted a type assignment to the term structure, ending up with a well-typed term. It would be very tedious for users to conduct such argumentation by hand for each term entered to HOL. Thus, HOL uses an adaptation of Milner's type inference algorithm for SML when constructing terms via parsing. At the end of type inference, unconstrained type variables get assigned names by the system. Usually, this assignment does the right thing. However, at times, the most general type is not what is desired and the user must add type constraints to the relevant subterms. For tricky situations, the global variable show_types can be assigned. When this flag is set, the prettyprinters for terms and theorems will show how types have been assigned to subterms. If you do not want the system to assign type variables for you, the global variable guessing_tyvars can be set to false, in which case the existence of unassigned type variables at the end of type inference will raise an exception.

Fixities

In order to provide some notational flexibility, constants come in various flavours or fixities: besides being an ordinary constant (with no fixity), constants can also be binders, prefixes, suffixes, infixes, or closefixes. More generally, terms can also be represented using reasonably arbitrary mixfix specifications. The degree to which terms bind their associated arguments is known as precedence. The higher this number, the tighter the binding. For example, when introduced, + has a precedence of 500, while the tighter binding multiplication (*) has a precedence of 600.

Binders

A binder is a construct that binds a variable; for example, the universal quantifier. In HOL, this is represented using a trick that goes back to Alonzo Church: a binder is a constant that takes a lambda abstraction as its argument. The lambda binding is used to implement the binding of the construct. This is an elegant and uniform solution. Thus the concrete syntax !v. M is represented by the application of the constant ! to the abstraction (\v. M).

The most common binders are !, ?, ?!, and @. Sometimes one wants to iterate applications of the same binder, e.g.,

   !x. !y. ?p. ?q. ?r. t.

This can instead be rendered

   !x y. ?p q r. t.
Infixes

Infix constants can associate in one of three different ways: right, left or not at all. (If + were non-associative, then 3 + 4 + 5 would fail to parse; one would have to write (3 + 4) + 5 or 3 + (4 + 5) depending on the desired meaning.) The precedence ordering for the initial set of infixes is /\, \/, ==>, =, , (comma4). Of these, equality is non-associative, and the remainder are right associative. Thus

   X /\ Y ==> C \/ D, P = E, Q

is equal to

   ((X /\ Y) ==> (C \/ D)), ((P = E), Q).

An expression $$ \term \; \langle\mathit{infix}\rangle\; \term $$ is internally represented as $$ ((\langle\mathit{infix}\rangle\; \term)\; \term) $$

Prefixes

Where infixes appear between their arguments, prefixes appear before theirs. This might initially appear to be the same thing as happens with normal function application where the symbol on the left simply has no fixity: is $f$ in $f(x)$ not acting as a prefix? Actually though, in a term such as $f(x)$, where $f$ and $x$ do not have fixities, the syntax is treated as if there is an invisible infix function application between the two tokens: $f\cdot{}x$. This infix operator binds tightly, so that when one writes $f\,x + y$, the parse is $(f\cdot{}x) + y$.5 It is then useful to allow for genuine prefixes so that operators can live at different precedence levels than function application. An example of this is ~, logical negation. This is a prefix with lower precedence than function application. Normally $$ f\;x\; y\qquad \text{is parsed as}\qquad (f\; x)\; y $$ but $$ \mathtt{\sim}\; x\; y\qquad\text{is parsed as}\qquad \mathtt{\sim}\; (x\; y) $$ because the precedence of ~ is lower than that of function application. The unary negation symbol would also typically be defined as a prefix, if only to allow one to write $$ \mathit{negop}\,\mathit{negop}\,3 $$ (whatever negop happened to be) without needing extra parentheses.

On the other hand, the univ syntax for the universal set (see Section 5.5.1) is an example of a prefix operator that binds more tightly than application. This means that f univ(:'a) is parsed as f(univ(:'a)), not (f univ)(:'a) (which parse would fail to type-check).

Suffixes

Suffixes appear after their arguments. There are suffixes ^+, ^* and ^= corresponding to the transitive, the reflexive and transitive, and the “equivalence” closure used in relationTheory (Section 5.5.3). Suffixes are associated with a precedence just as infixes and prefixes are. If p is a prefix, i an infix, and s a suffix, then there are six possible orderings for the three different operators based on their precedences, giving five parses for $\mathtt{p}\; t_1\; \mathtt{i}\; t_2\; \mathtt{s}$ depending on the relative precedences:

Precedences (lowest to highest)Parses
$p,\;i,\;s$$\mathtt{p}\;(t_1\;\mathtt{i}\;(t_2\;\mathtt{s}))$
$p,\;s,\;i$$\mathtt{p}\;((t_1\;\mathtt{i}\;t_2)\;\mathtt{s})$
$i,\;p,\;s$$(\mathtt{p}\;t_1)\;\mathtt{i}\;(t_2\;\mathtt{s})$
$i,\;s,\;p$$(\mathtt{p}\;t_1)\;\mathtt{i}\;(t_2\;\mathtt{s})$
$s,\;p,\;i$$(\mathtt{p}\;(t_1\;\mathtt{i}\;t_2))\;\mathtt{s}$
$s,\;i,\;p$$((\mathtt{p}\;t_1)\;\mathtt{i}\;t_2)\;\mathtt{s}$
Closefixes

Closefix terms are operators that completely enclose their arguments. An example one might use in the development of a theory of denotational semantics is semantic brackets. Thus, the HOL parsing facilities can be configured to allow one to write denotation x as [| x |]. Closefixes are not associated with precedences because they can not compete for arguments with other operators.

Parser tricks and magic

Here we describe how to achieve some useful effects with the parser in HOL.

Aliasing: If one wants a special syntax to be an “alias” for a normal HOL form, this is easy to achieve; both examples so far have effectively done this. However, if one just wants to have a normal one-for-one substitution of one string for another, one can't use the grammar/syntax phase of parsing to do this. Instead, one can use the overloading mechanism. For example, let us alias MEM for IS_EL. We need to use the function overload_on to overload the original constant for the new name:

   val _ = overload_on ("MEM", “IS_EL”);

Making addition right associative: If one has a number of old scripts that assume addition is right associative because this is how HOL used to be, it might be too much pain to convert. The trick is to remove all of the rules at the given level of the grammar, and put them back as right associative infixes. The easiest way to tell what rules are in the grammar is by inspection (use term_grammar()). With just arithmeticTheory loaded, the only infixes at level 500 are + and -. So, we remove the rules for them:

   val _ = app temp_remove_rules_for_term ["+", "-"];

And then we put them back with the appropriate associativity:

   val _ = app (fn s => temp_add_infix(s, 500, RIGHT)) ["+", "-"];

Note that we use the temp_ versions of these two functions so that other theories depending on this one won't be affected. Further note that we can't have two infixes at the same level of precedence with different associativities, so we have to remove both operators, not just addition.

Mix-fix syntax for if-then-else: The first step in bringing this about is to look at the general shape of expressions of this form. In this case, it will be: $$ \mathtt{if}\;\; \dots \;\;\mathtt{then}\;\;\dots\;\; \mathtt{else}\;\;\dots $$ Because there needs to be a “dangling” term to the right, the appropriate fixity is Prefix. Knowing that the underlying term constant is called COND, the simplest way to achieve the desired syntax is:

val _ = add_rule
   {term_name = "COND", fixity = Prefix 70,
    pp_elements = [TOK "if", BreakSpace(1,0), TM, BreakSpace(1,0),
                   TOK "then", BreakSpace(1,0), TM, BreakSpace(1,0),
                   TOK "else", BreakSpace(1,0)],
    paren_style = Always,
    block_style = (AroundEachPhrase, (PP.CONSISTENT, 0))};

The actual rule is slightly more complicated, and may be found in the sources for the theory bool.

Mix-fix syntax for term substitution: Here the desire is to be able to write something like: $$ \mathtt{[}\,t_1\,\mathtt{/}\,t_2\,\mathtt{]}\,t_3 $$ denoting the substitution of $t_1$ for $t_2$ in $t_3$, perhaps translating to SUB $t_1\;t_2\;t_3$. This looks like it should be another Prefix, but the choice of the square brackets ([ and ]) as delimiters would conflict with the concrete syntax for list literals if this was done. Given that list literals are effectively of the CloseFix class, the new syntax must be of the same class. This is easy enough to do: we set up syntax $$ \mathtt{[}\,t_1\,\mathtt{/}\,t_2\,\mathtt{]} $$ to map to SUB $t_1\;t_2$, a value of a functional type, that when applied to a third argument will look right.6 The rule for this is thus:

  val _ = add_rule
           {term_name = "SUB", fixity = Closefix,
            pp_elements = [TOK "[", TM, TOK "/", TM, TOK "]"],
            paren_style = OnlyIfNecessary,
            block_style = (AroundEachPhrase, (PP.INCONSISTENT, 2))};

Hiding constants

The following function can be used to hide the constant status of a name from the quotation parser.

  val hide   : string -> ({Name : string, Thy : string} list *
                          {Name : string, Thy : string} list)

Evaluating hide "$x$" makes the quotation parser treat $x$ as a variable (lexical rules permitting), even if $x$ is the name of a constant in the current theory (constants and variables can have the same name). This is useful if one wants to use variables with the same names as previously declared (or built-in) constants (e.g., o, I, S, etc.). The name $x$ is still a constant for the constructors, theories, etc.; hide affects parsing and printing by removing the given name from the “overload map” described above in Section 8.1.2.1. Note that the effect of hide is temporary; its effects do not persist in theories descended from the current one. See the REFERENCE entry for hide for more details, including an explanation of the return type.

The function

   reveal : string -> unit

undoes hiding.

The function

   hidden : string -> bool

tests whether a string is the name of a hidden constant.

Adjusting the pretty-print depth

The following SML reference can be used to adjust the maximum depth of printing

   max_print_depth : int ref

The default print depth is $-1$, which is interpreted as meaning no maximum. Subterms nested more deeply than the maximum print depth are printed as .... For example:

> arithmeticTheory.ADD_CLAUSES;
val it =
   ⊢ 0 + m = m ∧ m + 0 = m ∧ SUC m + n = SUC (m + n) ∧
     m + SUC n = SUC (m + n): thm

> max_print_depth := 3;
val it = (): unit
> arithmeticTheory.ADD_CLAUSES;
val it = ⊢ ... + ... = m ∧ ... = ... ∧ ... ∧ ...: thm

Quotations and antiquotation

Logic-related syntax in the HOL system is typically passed to the parser in special forms known as quotations. A basic quotation is delimited by single quotation characters (..., Unicode code-points U+2018 and U+2019), or single back-ticks (i.e., `, ASCII character 96). When quotation values are printed out by the ML interactive loop, they look rather ugly because of the special filtering that is done to these values before the ML interpreter even sees them:

> val q = ‘f x = 3’;
val q = [QUOTE " (*#loc 1 12*)f x = 3"]: 'a frag list

Quotations (the ML environment prints the type as 'a frag list) are the raw input form expected by the various HOL parsers. They are also polymorphic (to be explained below). Thus the function Parse.Term takes a (term) quotation and returns a term, and is of type $$ term quotation -> term $$

The term and type parsers can also be called implicitly by using double quotations (with “...”, characters U+201C and U+201D), or doubled back-ticks as delimiters. For the type parser, the first non-space character after the leading delimiter must also be a colon. Thus:

> val t1 = “\x:num. x + 3”;
val t1 = “λx. x + 3”: term
> val t2 = ``p /\ q``;
val t2 = “p ∧ q”: term

> val ty = “:'a -> bool”;
val ty = “:α -> bool”: hol_type

The expression bound to ML variable t1 above is actually expanded to an application of the function Parse.Term to the quotation argument ‘p /\ q’. Similarly, the declaration of ty’s expression expands into an application of Parse.Type to the quotation ‘:'a -> bool’.

The significant advantage of quotations over normal SML strings is that they can include new-line and backslash characters without requiring special quoting. Newlines occur whenever terms get beyond the trivial in size, while backslashes occur in not just the ASCII representation of $\lambda$, but also the syntax for conjunction and disjunction.

If a quotation is to include a back-quote character, then the single back-quote delimiters cannot be used; instead this must be done by using the ‘..’ syntax. Backquotes can also freely appear between the “...” delimiters. To include a caret in quoted material (otherwise used for antiquotations, see below), it must not be followed by an alphanumeric identifier, nor by parentheses. Thus:

> ‘f `x’;
val it = [QUOTE " (*#loc 1 4*)f `x"]: 'a frag list

> ``f ^` x``;
<<HOL message: inventing new type variable names: 'a, 'b, 'c>>
val it = “f ^` x”: term

> “f ^ x”;
<<HOL message: inventing new type variable names: 'a, 'b, 'c>>
val it = “f ^ x”: term

The main use of the caret is to introduce antiquotations (as suggested in the last example above). Within a quotation, expressions of the form ^($t$) (where $t$ is an SML expression of type term or type) are called antiquotations. An antiquotation ^($t$) evaluates to the SML value of $t$. For example, x \/ ^(mk_conj(“y:bool”, “z:bool”)) evaluates to the same term as x \/ (y /\ z).
The most common use of antiquotation is when the term $t$ is bound to an SML variable $x$. In this case ^($x$) can be abbreviated by ^$x$.

The following session illustrates antiquotation.

> load "intLib";   ... output elided ...
> val y = “x+1”;
val y = “x + 1”: term

> val z = “y = ^y”;
val z = “y = x + 1”: term

> “!x:num.?y:num.^z”;
val it = “∀x. ∃y. y = x + 1”: term

Types may be antiquoted as well:

> val pred = “:'a -> bool”;
val pred = “:α -> bool”: hol_type

> “:^pred -> bool”;
val it = “:(α -> bool) -> bool”: hol_type

Quotations are polymorphic, and the type variable of a quotation corresponds to the type of entity that can be antiquoted into that quotation. Because the term parser expects only antiquoted terms, antiquoting a type into a term quotation requires the use of ty_antiq. For example,

> “!P:^pred. P x ==> Q x”;
Exception- Type error in function application.
   Function: Parse.Term : term frag list -> term
   Argument:
      [
         QUOTE " (*#loc 1 4*)!P:",
         ANTIQUOTE pred,
         QUOTE " (*#loc 1 12*). P x ==> Q x"
         ] : hol_type frag list
   Reason:
      Can't unify term (*Created from opaque signature*) with
         hol_type (*Created from opaque signature*)
         (Different type constructors)
Fail "Static Errors" raised

> “!P:^(ty_antiq pred). P x ==> Q x”;
val it = “∀P. P x ⇒ Q x”: term

Generalised Quotation Syntax

In line with various “modern” syntaxes (e.g., Theorem and Definition), it is possible to define custom quotation tooling using the Quote keyword. There are two forms. The first is

Quote ident1 = ident2:
  ...
End

where the Quote and End keywords must occur in column 1 of the source script-file. This is translated to

val ident1 = ident2 ‘ ... ’;

The other form is a short-cut where the quotation is parsed by the provided function only for its side-effects. The surface syntax is

Quote ident:
  ...
End

which is translated to

val _ = ident ‘ ... ’;

In both forms the identifier immediately before the colon must be a valid SML identifier containing only alpha-numeric characters (but with structure-qualification and full-stops also permitted). In the first form, ident1 must be a simple (unqualified) alphanumeric identifier (e.g., foo, bar', Foo_bar, but not M.bar, nor ++).

Backwards compatibility of syntax

This section of the manual documents the (extensive) changes made to the parsing of HOL terms and types in the Taupo release (one of the HOL3 releases) and beyond from the point of view of a user who doesn't want to know how to use the new facilities, but wants to make sure that their old code continues to work cleanly.

The changes which may cause old terms to fail to parse are:

  • The precedence of type annotations has completely changed. It is now a very tight suffix (though with a precedence weaker than that associated with function application), instead of a weak one. This means that (x,y:bool # bool) should now be written as (x,y):bool # bool. The previous form will now be parsed as a type annotation applying to just the y. This change brings the syntax of the logic closer to that of SML and should make it generally easier to annotate tuples, as one can now write $$ (x\,:\,\tau_1,\;y\,:\,\tau_2,\dots z\,:\,\tau_n) $$ instead of $$ (x\,:\,\tau_1, \;(y\,:\,\tau_2, \dots (z\,:\,\tau_n))) $$ where extra parentheses have had to be added just to allow one to write a frequently occurring form of constraint.

  • Most arithmetic operators are now left associative instead of right associative. In particular, $+$, $-$, $*$ and DIV are all left associative. Similarly, the analogous operators in other numeric theories such as integer and real are also left associative. This brings the HOL parser in line with standard mathematical practice.

  • The binding equality in let expressions is treated exactly the same way as equalities in other contexts. In previous versions of HOL, equalities in this context have a different, weak binding precedence.

  • The old syntax for conditional expressions has been removed. Thus the string $p$=>$q$|$r$ must now be written ‘if $p$then$q$else$r$ instead.

  • Some lexical categories are more strictly policed. String literals (strings inside double quotes) and numerals can't be used unless the relevant theories have been loaded. Nor can these literals be used as variables inside binding scopes.

A Simple Interactive Proof Manager

The goal stack provides a simple interface to tactic-based interactive proof. When one uses tactics to decompose a proof, many intermediate states arise; the goalstack takes care of the necessary bookkeeping. The implementation of goalstacks reported here is a re-design of Larry Paulson's original conception.

The goalstack library is automatically loaded when HOL starts up. Editor modes can support the process of using the proof manager; here we describe the underlying SML interface.

The abstract types goalstack and proofs are the focus of backwards proof operations. The type proofs can be regarded as a list of independent goalstacks. Most operations act on the head of the list of goalstacks; there are also operations so that the focus can be changed.

Starting a goalstack proof

   g        : term quotation -> proofs
   set_goal : goal -> proofs

Recall that the type goal is an abbreviation for term list * term. To start on a new goal, one gives set_goal a goal. This creates a new goalstack and makes it the focus of further operations.

A shorthand for set_goal is the function g: it invokes the parser automatically, and it doesn't allow the goal to have any assumptions.

Calling set_goal, or g, adds a new proof attempt to the existing ones, i.e., rather than overwriting the current proof attempt, the new attempt is stacked on top.

Applying a tactic to a goal

   expandf : tactic -> goalstack
   expand  : tactic -> goalstack
   e       : tactic -> goalstack

How does one actually do a goalstack proof then? In most cases, the application of tactics to the current goal is done with the function expand. In the rare case that one wants to apply an invalid tactic, then expandf is used. (For an explanation of invalid tactics, see Section 4.1.) The abbreviation e may also be used to expand a tactic.

Undo

   b          : unit -> goalstack
   rd         : unit -> goalstack
   drop       : unit -> proofs
   dropn      : int  -> proofs
   backup     : unit -> goalstack
   redo       : unit -> goalstack
   restart    : unit -> goalstack
   set_backup : int  -> unit

Often (we are tempted to say usually!) one takes a wrong path in doing a proof, or makes a mistake when setting a goal. To undo a step in the goalstack, the function backup and its abbreviation b are used. This will restore the goalstack to its previous state. To redo a step in the goalstack, the redo function, abbreviated as rd, may be used.

To directly back up all the way to the original goal, the function restart may be used. Obviously, it is also important to get rid of proof attempts that are wrong; for that there is drop, which gets rid of the current proof attempt, and dropn, which eliminates the top $n$ proof attempts.

Each proof attempt has its own undo-list of previous states. The undo-list for each attempt is of fixed size (initially 12). If you wish to set this value for the current proof attempt, the function set_backup can be used. If the size of the backup list is set to be smaller than it currently is, the undo list will be immediately truncated. You can not undo a “proofs-level” operation, such as set_goal or drop.

Viewing the state of the proof manager

   p            : unit -> goalstack
   status       : unit -> proofs
   top_goal     : unit -> goal
   top_goals    : unit -> goal list
   initial_goal : unit -> goal
   top_thm      : unit -> thm

To view the state of the proof manager at any time, the functions p and status can be used. The former only shows the top subgoals in the current goalstack, while the second gives a summary of every proof attempt.

To get the top goal or goals of a proof attempt, use top_goal and top_goals. To get the original goal of a proof attempt, use initial_goal.

Once a theorem has been proved, the goalstack that was used to derive it still exists (including its undo-list): its main job now is to hold the theorem. This theorem can be retrieved with top_thm.

Switch focus to a different subgoal or proof attempt

   r             : int -> goalstack
   R             : int -> proofs
   rotate        : int -> goalstack
   rotate_proofs : int -> proofs

Often we want to switch our attention to a different goal in the current proof, or a different proof. The functions that do this are rotate and rotate_proofs, respectively. The abbreviations r and R are simpler to type in.

High Level Proof — bossLib

The library bossLib marshals some of the most widely used theorem proving tools in HOL and provides them with a convenient interface for interaction. The library currently focuses on three things: definition of datatypes and functions; high-level interactive proof operations, and composition of automated reasoners. Loading bossLib commits one to working in a context that already supplies the theories of booleans, pairs, sums, the option type, arithmetic, and lists.

Support for high-level proof steps

The following functions use information in the database to ease the application of HOL's underlying functionality:

   type_rws     : hol_type -> thm list
   Induct       : tactic
   Cases        : tactic
   Cases_on     : term quotation -> tactic
   Induct_on    : term quotation -> tactic

The function type_rws will search for the given type in the underlying TypeBase database and return useful rewrite rules for that type. The rewrite rules of the datatype are built from the injectivity and distinctness theorems, along with the case constant definition. The simplification tactics RW_TAC, SRW_TAC, and the simpset (srw_ss()) automatically include these theorems. Other tactics used with other simpsets will need these theorems to be manually added.

The Induct tactic makes it convenient to invoke induction. When it is applied to a goal, the leading universal quantifier is examined; if its type is that of a known datatype, the appropriate structural induction tactic is extracted and applied.

The Cases tactic makes it convenient to invoke case analysis. The leading universal quantifier in the goal is examined; if its type is that of a known datatype, the appropriate structural case analysis theorem is extracted and applied.

The Cases_on tactic takes a quotation, which is parsed into a term $M$, and then $M$ is searched for in the goal. If $M$ is a variable, then a variable with the same name is searched for. Once the term to split over is known, its type and the associated facts are obtained from the underlying database and used to perform the case split. If some free variables of $M$ are bound in the goal, an attempt is made to remove (universal) quantifiers so that the case split has force. Finally, $M$ need not appear in the goal, although it should at least contain some free variables already appearing in the goal. Note that the Cases_on tactic is more general than Cases, but it does require an explicit term to be given.

The Induct_on tactic takes a quotation, which is parsed into a term $M$, and then $M$ is searched for in the goal. If $M$ is a variable, then a variable with the same name is searched for. Once the term to induct on is known, its type and the associated facts are obtained from the underlying database and used to perform the induction. If $M$ is not a variable, a new variable $v$ not already occurring in the goal is created, and used to build a term $v = M$ which the goal is made conditional on before the induction is performed. First however, all terms containing free variables from $M$ are moved from the assumptions to the conclusion of the goal, and all free variables of $M$ are universally quantified. Induct_on is more general than Induct, but it does require an explicit term to be given.

Induct_on can also be used to perform rule inductions, for which see Section 7.7.1.

Three supplementary entry-points have been provided for more exotic inductions:

completeInduct_on: performs complete induction on the term denoted by the given quotation. Complete induction allows a seemingly7 stronger induction hypothesis than ordinary mathematical induction: to wit, when inducting on $n$, one is allowed to assume the property holds for all $m$ smaller than $n$. Formally: $\forall P.\ (\forall x.\ (\forall y.\ y < x \supset P\, y) \supset P\,x) \supset \forall x.\ P\,x$. This allows the inductive hypothesis to be used more than once, and also allows instantiating the inductive hypothesis to other than the predecessor.

measureInduct_on: takes a quotation, and breaks it apart to find a term and a measure function with which to induct. For example, if one wanted to induct on the length of a list L, the invocation measureInduct_on `LENGTH L` would be appropriate.

recInduct: takes a induction theorem generated by Define or Hol_defn and applies it to the current goal.

Automated reasoners

bossLib brings together the most powerful reasoners in HOL and tries to make it easy to compose them in a simple way. We take our basic reasoners from mesonLib, simpLib, and numLib, but the point of bossLib is to provide a layer of abstraction so the user has to know only a few entry-points.8 (These underlying libraries, and others providing similarly powerful tools are described in detail in sections below.)

   PROVE      : thm list -> term -> thm
   PROVE_TAC  : thm list -> tactic

   METIS_TAC  : thm list -> tactic
   METIS_PROVE: thm list -> term -> thm

   DECIDE     : term quotation -> thm
   DECIDE_TAC : tactic

The inference rule PROVE (and the corresponding tactic PROVE_TAC) takes a list of theorems and a term, and attempts to prove the term using a first order reasoner. The two METIS functions perform the same functionality but use a different underlying proof method. The PROVE entry-points refer to the meson library, which is further described in Section 8.4.1 below. The METIS system is described in Section 8.4.2. The inference rule DECIDE (and the corresponding tactic DECIDE_TAC) applies a decision procedure that (at least) handles statements of linear arithmetic.

   RW_TAC   : simpset -> thm list -> tactic
   SRW_TAC  : ssfrag list -> thm list -> tactic
   &&       : simpset * thm list -> simpset  (* infix *)
   std_ss   : simpset
   arith_ss : simpset
   list_ss  : simpset
   srw_ss   : unit -> simpset

The rewriting tactic RW_TAC works by first adding the given theorems into the given simpset; then it simplifies the goal as much as possible; then it performs case splits on any conditional expressions in the goal; then it repeatedly (1) eliminates all hypotheses of the form $v = M$ or $M = v$ where $v$ is a variable not occurring in $M$, (2) breaks down any equations between constructor terms occurring anywhere in the goal. Finally, RW_TAC lifts let-expressions within the goal so that the binding equations appear as abbreviations in the assumptions.

The tactic SRW_TAC is similar to RW_TAC, but works with respect to an underlying simpset (accessible through the function srw_ss) that is updated as new context is loaded. This simpset can be augmented through the addition of "simpset fragments" (ssfrag values) and theorems. In situations where there are many large types stored in the system, RW_TAC's performance can suffer because it repeatedly adds all of the rewrite theorems for the known types into a simpset before attacking the goal. On the other hand, SRW_TAC loads rewrites into the simpset underneath srw_ss() just once, making for faster operation in this situation.

bossLib provides a number of simplification sets. The simpset for pure logic, sums, pairs, and the option type is named std_ss. The simpset for arithmetic is named arith_ss, and the simpset for lists is named list_ss. The simpsets provided by bossLib strictly increase in strength: std_ss is contained in arith_ss, and arith_ss is contained in list_ss. The infix combinator \&\& is used to build a new simpset from a given simpset and a list of theorems. HOL's simplification technology is described further in Section 8.5 below and in the REFERENCE.

   by : term quotation * tactic -> tactic (* infix 8 *)
   SPOSE_NOT_THEN : (thm -> tactic) -> tactic

The function by is an infix operator that takes a quotation and a tactic $\mathit{tac}$. The quotation is parsed into a term $M$. When the invocation `M` by tac is applied to a goal $(A,g)$, a new subgoal $(A,M)$ is created and $\mathit{tac}$ is applied to it. If the goal is proved, the resulting theorem is broken down and added to the assumptions of the original goal; thus the proof proceeds with the goal $((M::A), g)$. (Note however, that case-splitting will happen if the breaking-down of $\vdash M$ exposes disjunctions.) Thus by allows a useful style of ‘assertional’ or ‘Mizar-like’ reasoning to be mixed with ordinary tactic proof.9

The SPOSE_NOT_THEN entry-point initiates a proof by contradiction by assuming the negation of the goal and driving the negation inwards through quantifiers. It provides the resulting theorem as an argument to the supplied function, which will use the theorem to build and apply a tactic.

First Order Proof — mesonLib and metisLib

First order proof is a powerful theorem-proving technique that can finish off complicated goals. Unlike tools such as the simplifier, it either proves a goal outright, or fails. It can not transform a goal into a different (and more helpful) form.

Model elimination — mesonLib

The meson library is an implementation of the model-elimination method for finding proofs of goals in first-order logic. There are three main entry-points:

   MESON_TAC     : thm list -> tactic
   ASM_MESON_TAC : thm list -> tactic
   GEN_MESON_TAC : int -> int -> int -> thm list -> tactic

Each of these tactics attempts to prove the goal. They will either succeed in doing so, or fail with a “depth exceeded” exception. If the branching factor in the search-space is high, the meson tactics may also take a very long time to reach the maximum depth.

All of the meson tactics take a list of theorems. These extra facts are used by the decision procedure to help prove the goal. MESON_TAC ignores the goal's assumptions; the other two entry-points include the assumptions as part of the sequent to be proved.

The extra parameters to GEN_MESON_TAC provide extra control of the behaviour of the iterative deepening that is at the heart of the search for a proof. In any given iteration, the algorithm searches for a proof of depth no more than a parameter $d$. The default behaviour for MESON_TAC and ASM_MESON_TAC is to start $d$ at 0, to increment it by one each time a search fails, and to fail if $d$ exceeds the value stored in the reference value mesonLib.max_depth. By way of contrast, GEN_MESON_TAC\ min\ max\ step starts $d$ at min, increments it by step, and gives up when $d$ exceeds max.

The PROVE_TAC function from bossLib performs some normalisation, before passing a goal and its assumptions to ASM_MESON_TAC. Because of this normalisation, in most circumstances, PROVE_TAC should be preferred to ASM_MESON_TAC.

Resolution — metisLib

The metis library is an implementation of the resolution method for finding proofs of goals in first-order logic. There are two main entry-points:

   METIS_TAC   : thm list -> tactic
   METIS_PROVE : thm list -> term -> thm

Both functions take a list of theorems, and these are used as lemmas in the proof. METIS_TAC is a tactic, and will either succeed in proving the goal, or if unsuccessful will either fail or loop forever. METIS_PROVE takes a term $t$ and tries to prove a theorem with conclusion $t$: if successful, the theorem $\vdash t$ is returned. As for METIS_TAC, it might fail or loop forever if the proof search is unsuccessful.

The metisLib family of proof tools implement the ordered resolution and ordered paramodulation calculus for first order logic, which usually makes them better suited to goals requiring non-trivial equality reasoning than the tactics in mesonLib.

Efficient Applicative Order Reduction — computeLib

Section 7.2 and Section 7.6 show the ability of HOL to represent many of the standard constructs of functional programming. If one then wants to ‘run’ functional programs on arguments, there are several choices. First, one could apply the simplifier, as demonstrated in Section 8.5. This allows all the power of the rewriting process to be brought to bear, including, for example, the application of decision procedures to prove constraints on conditional rewrite rules. Second, one could write the program, and all the programs it transitively depends on, out to a file in a suitable concrete syntax, and invoke a compiler or interpreter. This functionality is available in HOL via use of EmitML.exportML.

Third, computeLib can be used. This library supports call-by-value evaluation of HOL functions by deductive steps. In other words, it is quite similar to having an SML interpreter inside the HOL logic, working by forward inference. When used in this way, functional programs can be executed more quickly than by using the simplifier.

The most accessible entry-points for using the computeLib library are the conversion EVAL and its tactic counterpart EVAL_TAC. These depend on an internal database that stores function definitions. In the following example, loading sortingTheory augments this database with relevant definitions, that of Quicksort (QSORT) in particular, and then we can evaluate QSORT on a concrete list.

> load "sortingTheory";
val it = (): unit

> EVAL ``QSORT (<=) [76;34;102;3;4]``;
<<HOL message: more than one resolution of overloading was possible>>
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ QSORT $<= [76; 34; 102; 3; 4] = QSORT $<= [76; 34; 102; 3; 4]: thm

Often, the argument to a function has no variables: in that case application of EVAL ought to return a ground result, as in the above example. However, EVAL can also evaluate functions on arguments with variables — so-called symbolic evaluation — and in that case, the behaviour of EVAL depends on the structure of the recursion equations. For example, in the following session, if there is sufficient information in the input, symbolic execution can deliver an interesting result. However, if there is not enough information in the input to allow the algorithm any traction, no expansion will take place.

> EVAL ``REVERSE [u;v;w;x;y;z]``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ REVERSE [u; v; w; x; y; z] = [z; y; x; w; v; u]: thm

> EVAL ``REVERSE alist``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ REVERSE alist = REV alist []: thm

Dealing with divergence

The major difficulty with using EVAL is termination. All too often, symbolic evaluation with EVAL will diverge, or generate enormous terms. The usual cause is conditionals with variables in the test. For example, the following definition is provably equal to FACT,

> Define `fact n = if n=0 then 1 else n * fact (n-1)`;
<<HOL warning: ThmSetData.revise_data: 
  Theorems in set "compute":
    ADD<scratch$fact_def>
  invalidated by NewBinding("fact_def", {private=false,loc=Unknown,class=Thm})>>
Equations stored under "fact_def".
Induction stored under "fact_ind".
val it = ⊢ ∀n. fact n = if n = 0 then 1 else n * fact (n − 1): thm

But the two definitions evaluate completely differently.

> EVAL ``FACT n``;
val it = ⊢ FACT n = FACT n: thm

> EVAL ``fact n``;
  <.... interrupt key struck ...>
Interrupted.

The primitive-recursive definition of FACT does not expand at all, while the destructor-style recursion of fact never stops expanding. A rudimentary monitoring facility shows the behaviour, first on a ground argument, then on a symbolic argument.

> val [fact] = decls "fact";
val fact = “fact”: term
> computeLib.monitoring := SOME (same_const fact);
val it = (): unit

> EVAL ``fact 4``;
fact 4 = if 4 = 0 then 1 else 4 * fact (4 − 1)
fact 3 = if 3 = 0 then 1 else 3 * fact (3 − 1)
fact 2 = if 2 = 0 then 1 else 2 * fact (2 − 1)
fact 1 = if 1 = 0 then 1 else 1 * fact (1 − 1)
fact 0 = if 0 = 0 then 1 else 0 * fact (0 − 1)
val it = ⊢ fact 4 = 24: thm

> EVAL ``fact n``;
fact n = (if n = 0 then 1 else n * fact (n - 1))
fact (n - 1) = (if n - 1 = 0 then 1 else (n - 1) * fact (n - 1 - 1))
fact (n - 1 - 1) =
(if n - 1 - 1 = 0 then 1 else (n - 1 - 1) * fact (n - 1 - 1 - 1))
fact (n - 1 - 1 - 1) =
(if n - 1 - 1 - 1 = 0 then
   1
 else
   (n - 1 - 1 - 1) * fact (n - 1 - 1 - 1 - 1))
   .
   .
   .

In each recursive expansion, the test involves a variable, and hence cannot be reduced to either T or F. Thus, expansion never stops.

Some simple remedies can be adopted in trying to deal with non-terminating symbolic evaluation.

  • RESTR_EVAL_CONV behaves like EVAL except it takes an extra list of constants. During evaluation, if one of the supplied constants is encountered, it will not be expanded. This allows evaluation down to a specified level, and can be used to cut-off some looping evaluations.

  • set_skip can also be used to control evaluation. See the REFERENCE entry for CBV_CONV for discussion of set_skip.

Custom evaluators

For some problems, it is desirable to construct a customized evaluator, specialized to a fixed set of definitions. The compset type found in computeLib is the type of definition databases. The functions new_compset, bool_compset, add_funs, and add_convs provide the standard way to build up such databases. Another quite useful compset is reduceLib.num_compset, which may be used for evaluating terms with numbers and booleans. Given a compset, the function CBV_CONV generates an evaluator: it is used to implement EVAL. See REFERENCE for more details.

Dealing with Functions over Peano Numbers

Functions defined by pattern-matching over Peano-style numbers cannot be used by EVAL to compute the application of those functions to numerals. This is because numerals are represented with a binary positional notation (described in Section 5.3.3). The Peano-style presentation is important for proofs about these functions, so we encourage definitions in this style. For the sake of computation, HOL's definitional facilities will automatically use the SML function numLib.SUC_TO_NUMERAL_DEFN_CONV to derive equations over numerals from an equation over SUC.

> Definition mod3_def:
    mod3 0 = 0 /\
    mod3 (SUC n) = let m = mod3 n in
                   if m = 2 then 0 else m + 1
  End   ... output elided ...
> EVAL “mod3 11”;
val it = ⊢ mod3 11 = 2: thm

Storing and using definitions

HOL's top-level definition facilities (i.e., the Define function and the Definition syntax) automatically add definitions to the global compset used by EVAL and EVAL_TAC.

Occasionally, one does not want a definition automatically added to the global compset. The easiest way to achieve this is to use the nocompute “pseudo-attribute”:10

> Definition f_def[nocompute]: f x = x + 10
  End
Definition has been stored under "f_def"
val f_def = ⊢ ∀x. f x = x + 10: thm
> EVAL ``f 6``;
val it = ⊢ f 6 = f 6: thm

By using the nocompute attribute, or lower level tools such as Hol_defn, defining equations are not added to the global compset. Subsequently, one may want to specify theorems to be used as the basis for EVAL's computation. This can be done with the compute attribute attached to a theorem declaration. For example, one might write

> Theorem f_6[compute]: f 6 = 16
  Proof simp[f_def]
  QED
val f_6 = ⊢ f 6 = 16: thm

> EVAL “f 6 = f 7”;
val it = ⊢ f 6 = f 7 ⇔ 16 = f 7: thm

Computation with First-Order Terms — cv_computeLib and cv_transLib

The library cv_computeLib supports fast evaluation of HOL terms. It exports a single conversion, cv_compute, which accepts terms in a simple, first-order, untyped language built from a recursive datatype of pairs and natural numbers. Its accompanying theory, cvTheory, defines this type (called :cv) and its operations. When applicable, cv_computeLib will often execute several orders of magnitude faster than computeLib (Section 8.6) on similar inputs. Such performance is possible because cv_computeLib relies on an interpreter that is implemented inside the kernel, and that uses native ML datatypes and arbitrary precision integer arithmetic to execute quickly. This implementation is a Standard ML adaption of the compute facility in the Candle theorem prover, which has been proved to be sound wrt. the inference rules of higher-order logic (Abrahamsson and Myreen 2023).

The library cv_transLib provides a user-friendly interface to cv_computeLib. It exports automation which translates functional HOL definitions into equivalent functions which operate over the :cv type, and a wrapper around cv_computeLib.cv_compute called cv_eval which can be used much like computeLib.EVAL (Section 8.6). Its accompanying theories (cv_primTheory and cv_stdTheory) define and translate various common operations over primitive types.

Computing with cv_compute directly

The following example shows how to define a (very simple) function and use it in a computation with cv_compute.

NB: this example illustrates how cv_compute works, but is not the recommended workflow for using it. Instead, use cv_transLib (Section 8.7.3).

> load "cv_computeLib";
val it = (): unit
> Definition square_def:
    square x = cv_mul x x
  End
Definition has been stored under "square_def"
val square_def = ⊢ ∀x. square x = cv_mul x x: thm
> cv_computeLib.cv_compute [square_def] ``square (cv$Num 7)``;
val it = ⊢ square (cv$Num 7) = cv$Num 49: thm

To reduce a term involving the constant square, cv_compute must be given its defining equation (square_def), and both the input term and this equation must be written in a special style, using a special set of operations (such as cv_mul). We call defining equations and terms in this style code equations and compute expressions, respectively.

Compute expressions

A compute expression is a closed, first-order expression with type :cv. The :cv datatype is defined in cvTheory as follows:

  Datatype: cv = Pair cv cv
               | Num num
  End

Aside from the :cv datatype constructors, the following operations can be used to construct new compute expressions:

Arithmetic

OperationDescription
cv_add: cv -> cv -> cvAddition
cv_sub: cv -> cv -> cvSubtraction
cv_mul: cv -> cv -> cvMultiplication
cv_div: cv -> cv -> cvDivision (defined for zero)
cv_mod: cv -> cv -> cvModulus (defined for zero)
cv_lt: cv -> cv -> cvLess-than (<) comparison

Pairs

OperationDescription
cv_fst: cv -> cvFirst pair projection
cv_snd: cv -> cvSecond pair projection
cv_ispair: cv -> cvPair recognizer

Miscellaneous

OperationDescription
cv_eq: cv -> cv -> cvEquality
cv_if: cv -> cv -> cv -> cvif-then-else
let $x\;=\;y$ in $z$Let-binding: $x, y, z$ must have type :cv
$\mathtt{f}\;x_1\;\cdots\;x_n$Function application: all $x_i$ are of type :cv
$x$Variable: with type :cv

The following holds for the semantics of these operations:

  • Arithmetic works as on HOL's natural numbers.
  • Boolean-like expressions (such as cv_if) treat Num 1 as true, and all other values as false.
  • All ill-typed expressions (such as cv_fst (Num 3)) are defined as Num 0.
  • Function constants must have a corresponding code equation, see below.

Code equations

A theorem $\mathtt{f}\;x_1 \cdots x_n = e$ is a code equation for f, if:

  • $x_1 \cdots x_n$ are variables of type :cv,
  • $e$ has type :cv,
  • $e$ is a compute expression, except that the variables $x_i$ may be free in $e$.

Example: computing factorial

The following example is taken from the cv_computeLib examples, available in exampleTheory in examples/cv_compute, and has been modified to showcase let-bindings:

> load "cv_computeLib";
val it = (): unit

> Definition fact_def:
   fact n =
     let one = cv$Num 1 in
     cv_if (cv_lt n one)
           one
           (cv_mul n (fact (cv_sub n one)))
  Termination
   WF_REL_TAC `measure cv_size` >>
   Cases >>
   simp [cv_size_def, CaseEq "bool", c2b_def]
  End   ... output elided ...

> time (cv_computeLib.cv_compute [fact_def]) “fact (cv$Num 1234)”;
runtime: 0.11815s,    gctime: 0.00000s,     systime: 0.00068s.
val it =
   ⊢ fact (cv$Num 1234) =
     cv$Num
       51084981466469576881306176261004598750272741624636207875758364885...
   thm

On a modern machine, the call to cv_compute finishes in less than two tenths of a second.

Thm.compute

The conversion cv_compute is built on top of a kernel primitive accessible through the function Thm.compute:

  type instantiation =
    { cval_terms : (string * term) list,
      cval_type  : hol_type,
      num_type   : hol_type,
      char_eqns  : (string * thm) list }
  val compute : instantiation -> thm list -> term -> thm

Before it can be used, Thm.compute must be instantiated with a record containing constants, types and characteristic theorems for the constants and types. The list of characteristic equations is rather large, and need only be passed to compute once (the application is cached). Indeed, this instantiation is the sole duty performed by cv_computeLib.

The reason for why this instantiation must occur is as follows. Internally, Thm.compute takes apart the HOL logic's terms and converts them into its own representation, performs computation, and converts the result back into a term. The soundness of this procedure depends on various constants and types having certain meanings; for example, that + is natural number addition, and that the following holds:

ADD
  ⊢ (∀n. 0 + n = n) ∧ ∀m n. SUC m + n = SUC (m + n)

However, natural numbers, arithmetic, and theorems like ADD are derived long after the kernel code is compiled.

Alternative instantiations

It is possible to instantiate Thm.compute differently, for example if one wants to use a different type of numbers (as long as it satisfies the same axioms). An example instantiation can be seen in the source code of cv_computeLib in the HOL sources; the list of types, constants and symbols required is too long to include here.

Further reading

For an in-depth explanation of Thm.compute, we refer the reader to the Candle theorem prover's compute primitive (Abrahamsson and Myreen 2023), on which Thm.compute is based.

Computing with cv_compute via cv_transLib

It is also possible to use cv_compute on HOL functions that are not defined using the :cv type, using cv_transLib. This library supports automatic translation of functions to equivalent versions operating over the :cv type, and maintains a database of known translations. Then, the cv_eval entrypoint can be used on a regular HOL term: it uses the database to translate the term to an equivalent :cv version, invokes cv_compute, and translates the result back from the :cv type. Note therefore that, like cv_compute, cv_eval accepts only closed terms. All constants in its input must also be found in its database of known translations. Therefore, the intended workflow for using cv_transLib is as follows:

  1. Define HOL functions in the usual way.
  2. Invoke cv_transLib to translate these functions to :cv versions, populating the database of known translations.
  3. Use cv_transLib.cv_eval to evaluate terms composed of known constants efficiently.

Translation entrypoints

There are eight entrypoints in cv_transLib for translating HOL functions to :cv equivalents: cv_trans, cv_trans_pre, cv_trans_rec, cv_trans_rec_pre, cv_auto_trans, cv_auto_trans_pre, cv_auto_trans_rec, and cv_auto_trans_rec_pre.

All accept a theorem representing a HOL definition. Those with prefix cv_trans fail if, during translation, they encounter a constant which does not have a known translation. Those with prefix cv_auto_trans invoke themselves recursively on any such unknown constants.

Translation of some HOL functions give rise to a precondition (for example, listheory.HD requires its argument to be non-empty). All eight entrypoints will attempt to discharge simple preconditions, but the variants containing pre allow a more complex precondition to persist and return its definition to the user. Preconditions bubble up through further translations, and must be discharged for any term used with cv_eval. The other variants fail if they encounter a precondition they cannot discharge.

Translation of a recursive HOL function produces a recursive :cv function, which may require a termination proof. All eight entrypoints attempt to discharge simple termination proofs, but the variants containing rec accept an additional argument, a tactic which should discharge a more complex termination goal. The other variants will fail if they cannot prove termination.

Examples: computing squares and factorials

The following example mirrors those in Section 8.7.1, but it uses cv_transLib instead of interacting directly with the :cv type.

> load "cv_transLib";
val it = (): unit
> load "cv_stdTheory";
val it = (): unit

> Definition square'_def:
    square' (x:num) = x * x
  End
Definition has been stored under "square'_def"
val square'_def = ⊢ ∀x. square' x = x * x: thm

> cv_transLib.cv_trans square'_def;
val it = (): unit

> cv_transLib.cv_eval ``square' 7``;
val it = ⊢ square' 7 = 49: thm

> arithmeticTheory.FACT;
val it = ⊢ FACT 0 = 1 ∧ ∀n. FACT (SUC n) = SUC n * FACT n: thm

> cv_transLib.cv_trans arithmeticTheory.FACT;
Equations stored under "cv_FACT_def".
Induction stored under "cv_FACT_ind".
val it = (): unit

> time cv_transLib.cv_eval ``FACT 1234``;
runtime: 0.12709s,    gctime: 0.00000s,     systime: 0.00026s.
val it =
   ⊢ FACT 1234 =
     5108498146646957688130617626100459875027274162463620787575836488567...
   thm

Further usage examples are located in examples/cv_compute.

Arithmetic Libraries — numLib, intLib and realLib

Each of the arithmetic libraries of HOL provide a suite of definitions and theorems as well as automated inference support.

numLib

The most basic numbers in HOL are the natural numbers. The numLib library encompasses the theories numTheory, prim_recTheory, arithmeticTheory, and numeralTheory. This library also incorporates an evaluator for numeric expression from reduceLib and a decision procedure for linear arithmetic ARITH_CONV. The evaluator and the decision procedure are integrated into the simpset arith_ss used by the simplifier. As well, the linear arithmetic decision procedure can be directly invoked through DECIDE and DECIDE_TAC, both found in bossLib.

intLib

The intLib library comprises integerTheory, an extensive theory of the integers, plus two decision procedures for full Presburger arithmetic. These are available as intLib.COOPER_CONV and intLib.ARITH_CONV. These decision procedures are able to deal with linear arithmetic over the integers and the natural numbers, as well as dealing with arbitrary alternation of quantifiers. The ARITH_CONV procedure is an implementation of the Omega Test, and seems to generally perform better than Cooper's algorithm. There are problems for which this is not true however, so it is useful to have both procedures available.

In addition, the intLib.INTEGER_RULE (and its tactic version intLib.INTEGER_TAC) ported from HOL-Light can solve some simple equations about divisibility of integers, e.g. d int_divides m ==> d int_divides (m * n). As part of the procedure, multivariate polynomials of integer are expanded to their “normal forms” (with respect to certain ordering), and thus equations between equivalent such polynomials can be decided, e.g. w * y + x * z - (w * z + x * y) = (w - x) * (y - z).

realLib

The realLib library provides a foundational development of the real numbers and analysis. See Section 5.3.6 for a quick description of the theories. Also provided is a theory of polynomials, in polyTheory. A decision procedure for linear arithmetic on the real numbers is also provided by realLib, under the name REAL_ARITH_CONV and REAL_ARITH_TAC.

Bit Vector Library — wordsLib

The library wordsLib provides tool support for bit-vectors, this includes facilities for: evaluation, parsing, pretty-printing and simplification.

Evaluation

The library wordsLib should be loaded when evaluating ground bit-vector terms. This library provides a compset words_compset, which can be used in the construction of custom compsets and conversions.

> load "wordsLib";
val it = (): unit

> EVAL ``8w + 9w:word4``;
val it = ⊢ 8w + 9w = 1w: thm

Note that a type annotation is used here to designate the word size. When the word size is represented by a type variable (i.e., for arbitrary length words), evaluation may give partial or unsatisfactory results.

Parsing and pretty-printing

Words can be parsed in binary, decimal and hexadecimal. For example:

> ``0b111010w : word8``;
val it = “58w”: term

> ``0x3Aw : word8``;
val it = “58w”: term

It is possible to parse octal numbers, but this must be enabled first by setting the reference base_tokens.allow_octal_input to true. For example:

> ``072w : word8``;
val it = “72w”: term

> base_tokens.allow_octal_input:=true;
val it = (): unit

> ``072w : word8``;
val it = “58w”: term

Words can be pretty-printed using the standard number bases. For example, the function wordsLib.output_words_as_bin will select binary format:

> wordsLib.output_words_as_bin();
val it = (): unit

> EVAL ``($FCP ODD):word16``;
val it = ⊢ $FCP ODD = 0b1010101010101010w: thm

The function output_words_as is more flexible and allows the number base to vary depending on the word length and numeric value. The default pretty-printer (installed when loading wordsLib) prints small values in decimal and large values in hexadecimal. The function output_words_as_oct will automatically enable the parsing of octal numbers.

The trace variable "word printing" provides an alternative method for changing the output number base — it is particularly suited to temporarily selecting a number base, for example:

> Feedback.trace ("word printing", 1) Parse.term_to_string ``32w``;
<<HOL message: inventing new type variable names: 'a>>
val it = "0b100000w": string

The choices are as follows: 0 (default) — small numbers decimal, large numbers hexadecimal; 1 — binary; 2 — octal; 3 — decimal; and 4 — hexadecimal.

Types

You may have noticed that :word4 and :word8 have been used as convenient parsing abbreviations for :bool[4] and :bool[8] — this facility is available for many standard word sizes. Users wishing to use this notation for non-standard word sizes can use the function wordsLib.mk_word_size:

> Lib.try Parse.Type `:word15` handle _ => bool;

Exception raised at Parse.type parser: on line 1, characters 21-26:
  word15 not a known type operator
val it = “:bool”: hol_type

> wordsLib.mk_word_size 15;
val it = (): unit

> ``:word15``;
val it = “:word15”: hol_type

Operator overloading

The symbols for the standard arithmetic operations (addition, subtraction and multiplication) are overloaded with operators from other standard theories, i.e., for the natural, integer, rational and real numbers. In many cases type inference will resolve overloading, however, in some cases this is not possible. The choice of operator will then depend upon the order in which theories are loaded. To change this behaviour the functions wordsLib.deprecate_word and wordsLib.prefer_word are provided. For example, in the following session, the selection of word operators is deprecated:

> type_of ``a + b``;
<<HOL message: more than one resolution of overloading was possible>>
<<HOL message: inventing new type variable names: 'a>>
val it = “:α word”: hol_type

> wordsLib.deprecate_word();
val it = (): unit

> type_of ``a + b``;
<<HOL message: more than one resolution of overloading was possible>>
val it = “:num”: hol_type

In the above, natural number addition is chosen in preference to word addition. Conversely, words are preferred over the integers below:


> type_of ``a + b``;
<<HOL message: more than one resolution of overloading was possible>>
val it = “:num”: hol_type

> wordsLib.prefer_word();
val it = (): unit
> type_of ``a + b``;
<<HOL message: more than one resolution of overloading was possible>>
<<HOL message: inventing new type variable names: 'a>>
val it = “:α word”: hol_type

Of course, type annotations could have been added to avoid this problem entirely.

Guessing word lengths

It can be a nuisance to add type annotations when specifying the return type for operations such as: word_extract, word_concat, concat_word_list and word_replicate. This is because there is often a “standard” length that could be guessed, e.g., concatenation usually sums the constituent word lengths. A facility for word length guessing is controlled by the reference wordsLib.guessing_word_lengths, which is false by default. The guesses are made during a post-processing step that occurs after the application of Parse.Term. This is demonstrated below.

> wordsLib.guessing_word_lengths:=true;
val it = (): unit

> ``concat_word_list [(4 >< 1) (w:word32); w2; w3]``;
<<HOL message: inventing new type variable names: 'a, 'b>>
<<HOL message: assigning word length: α <- 4>>
<<HOL message: assigning word length: β <- 12>>
val it = “concat_word_list [(4 >< 1) w; w2; w3]”: term

In the example above, word length guessing is turned on. Two guesses are made: the extraction is expected to give a four bit word, and the concatenation gives a twelve bit word ($3 \times 4$). If non-standard numeric lengths are required then type annotations can be added to avoid guesses being made. With guessing turned off, the result types would remain as invented type variables, i.e., as alpha and beta above.

Simplification and conversions

The following simpset fragments are provided:

SIZES_ss: evaluates a group of functions that operate over numeric types, such as dimindex and dimword.

BIT_ss: tries to simplify occurrences of the function BIT.

WORD_LOGIC_ss: simplifies bitwise logic operations.

WORD_ARITH_ss: simplifies word arithmetic operations. Subtraction is replaced with multiplication by -1.

WORD_SHIFT_ss: simplifies shift operations.

WORD_ss: contains all of the above fragments, and also does some extra ground term evaluation. This fragment is added to srw_ss.

WORD_ARITH_EQ_ss: simplifies `a = b` to `a - b = 0w`.

WORD_BIT_EQ_ss: aggressively expands non-arithmetic bit-vector operations into Boolean expressions. (Should be used with care — it includes fcpLib.FCP_ss.)

WORD_EXTRACT_ss: simplification for a variety of operations: word-to-word conversions; concatenation; shifts and bit-field extraction. Can be used in situations where WORD_BIT_EQ_ss is unsuitable.

WORD_MUL_LSL_ss: simplifies multiplication by a word literal into a sum of partial products.

Many of these simpset fragments have corresponding conversions. For example, the conversion WORD_ARITH_CONV is based on WORD_ARITH_EQ_ss, however, it does some extra work to ensure that `a = b` and `b = a` convert into the same expression. Therefore, this conversion is suited to reasoning about the equality of arithmetic word expressions.

The behaviour of the fragments listed above are demonstrated using the following function:

> fun conv ss = SIMP_CONV (pure_ss++ss) [];
val conv = fn: ssfrag -> conv

The following session demonstrates SIZES_ss:

> conv wordsLib.SIZES_ss ``dimindex(:12)``;
val it = ⊢ dimindex (:12) = 12: thm

> conv wordsLib.SIZES_ss ``FINITE univ(:32)``;
val it = ⊢ FINITE 𝕌(:32) ⇔ T: thm

The fragment BIT_ss converts BIT into membership test over a set of (high) bit positions:

> conv wordsLib.BIT_ss ``BIT 3 5``;
val it = ⊢ BIT 3 5 ⇔ F: thm

> conv wordsLib.BIT_ss ``BIT i 123``;
val it = ⊢ BIT i 123 ⇔ i ∈ {0; 1; 3; 4; 5; 6}: thm

This simplification provides some support for reasoning about bitwise operations over arbitrary word lengths. The arithmetic, logic and shift fragments help tidy up basic word expressions:

> conv wordsLib.WORD_LOGIC_ss ``a && 12w || 11w && a``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ a && 12w ‖ 11w && a = 15w && a: thm

> conv wordsLib.WORD_ARITH_ss ``3w * b + a + 2w * b - a * 4w:word2``;
val it = ⊢ 3w * b + a + 2w * b − a * 4w = a + b: thm

> conv wordsLib.WORD_SHIFT_ss ``0w << 12 + a >>> 0 + b << 2 << 3``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ 0w ≪ 12 + a ⋙ 0 + b ≪ 2 ≪ 3 = 0w + a + b ≪ (2 + 3): thm

The remaining fragments are not included in wordsLib.WORD_ss or srw_ss. The bit equality fragment is demonstrated below.

> SIMP_CONV (std_ss++wordsLib.WORD_BIT_EQ_ss) [] ``a && b = ~0w : word2``;
val it = ⊢ a && b = ¬0w ⇔ (a ' 1 ∧ b ' 1) ∧ a ' 0 ∧ b ' 0: thm

The extract fragment is useful for reasoning about bit-field operations and is best used in combination with wordsLib.SIZES_ss or wordsLib.WORD_ss, for example:

> SIMP_CONV (std_ss++wordsLib.SIZES_ss++wordsLib.WORD_EXTRACT_ss) []
    ``(4 -- 1) ((a:word3) @@ (b:word2)) : word5``;
val it = ⊢ (4 -- 1) (a @@ b) = (2 >< 0) a ≪ 1 ‖ (1 >< 1) b: thm

Finally, the fragment WORD_MUL_LSL_ss is demonstrated below.

> conv wordsLib.WORD_MUL_LSL_ss ``5w * a : word8``;
val it = ⊢ 5w * a = a ≪ 2 + a: thm

Rewriting with the theorem wordsTheory.WORD_MUL_LSL provides an means to undo this simplification, for example:

> SIMP_CONV (std_ss++wordsLib.WORD_ARITH_ss) [wordsTheory.WORD_MUL_LSL]
    ``a << 2 + a : word8``;
val it = ⊢ a ≪ 2 + a = 5w * a: thm

Obviously, without adding safeguards, this rewrite theorem cannot be deployed when used in combination with the WORD_MUL_LSL_ss fragment.

Decision procedures

A decision procedure for words is provided in the form of blastLib.BBLAST_PROVE. This procedure uses bit-blasting — converting word expressions into propositions and then using a SAT solver to decide the goal.11 This approach is reasonably general and can tackle a wide range of bit-vector problems. However, there are some limitations: the approach only works for constant word lengths, linear arithmetic (multiplication by literals) and for shifts and bit-field extractions with respect to literal values. Also note that some problems will be potentially slow to prove, e.g., when word sizes are large and/or when there are many nested additions (perhaps through multiplication).

The following examples show BBLAST_PROVE in use:

> load "blastLib";   ... output elided ...
> blastLib.BBLAST_PROVE ``a + 2w <+ 4w <=> a <+ 2w \/ 13w <+ a :word4``;
val it = ⊢ a + 2w <₊ 4w ⇔ a <₊ 2w ∨ 13w <₊ a: thm

> blastLib.BBLAST_PROVE ``w2w (a:word8) <+ 256w : word16``;
val it = ⊢ w2w a <₊ 256w: thm

The decision procedure BBLAST_PROVE is based on the conversion BBLAST_CONV. This conversion can be used to convert bit-vector problems into a propositional form; for example:

> blastLib.BBLAST_CONV ``(((a : word16) + 5w) << 3) ' 5``;
val it = ⊢ ((a + 5w) ≪ 3) ' 5 ⇔ (¬a ' 2 ⇔ ¬(a ' 1 ∧ a ' 0)): thm

There are also bit-blasting tactics: BBLAST_TAC and FULL_BBLAST_TAC; with only the latter making use of goal assumptions.


  1. The types in Absyn constraints are not full HOL types, but values from another intermediate type, Pretype.

  2. The use of local induces a call to the temp_overload_on function.

  3. The matching done is first-order; contrast the higher-order matching done in the simplifier.

  4. When pairTheory has been loaded.

  5. There are tighter infix operators: the dot in field selection causes $f\,x.fld$ to parse as $f\cdot(x.fld)$.

  6. Note that doing the same thing for the if-then-else example in the previous example would be inappropriate, as it would allow one to write $$ \mathtt{if}\;P\;\mathtt{then}\;Q\;\mathtt{else} $$ without the trailing argument.

  7. Complete induction and ordinary mathematical induction are each derivable from the other.

  8. In the mid 1980's Graham Birtwistle advocated such an approach, calling it ‘Ten Tactic HOL’.

  9. Proofs in the Mizar system are readable documents, unlike most tactic-based proofs.

  10. The nocompute attribute does nothing when applied to Theorem declarations.

  11. This approach enables counter-examples to be given when a goal's negation is satisfiable.

Simplification — simpLib

Simplification — simpLib

The simplifier is HOL’s most sophisticated rewriting engine. It is recommended as a general purpose work-horse during interactive theorem-proving. As a rewriting tool, the simplifier’s general role is to apply theorems of the general form $$ \vdash l = r $$ to terms, replacing instances of $l$ in the term with $r$. Thus, the basic simplification routine is a conversion, taking a term $t$, and returning a theorem $\vdash t = t'$, or the exception UNCHANGED.

The basic conversion is

   simpLib.SIMP_CONV : simpLib.simpset -> thm list -> term -> thm

The first argument, a simpset, is the standard way of providing a collection of rewrite rules (and other data, to be explained below) to the simplifier. There are simpsets accompanying most of HOL’s major theories. For example, the simpset bool_ss in boolSimps embodies all of the usual rewrite theorems one would want over boolean formulas:

> SIMP_CONV bool_ss [] ``p /\ T \/ ~(q /\ r)``;
val it = ⊢ p ∧ T ∨ ¬(q ∧ r) ⇔ p ∨ ¬q ∨ ¬r: thm

In addition to rewriting with the obvious theorems, bool_ss is also capable of performing simplifications that are not expressible as simple theorems:

> SIMP_CONV bool_ss [] ``?x. (\y. P (f y)) x /\ (x = z)``;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = ⊢ (∃x. (λy. P (f y)) x ∧ x = z) ⇔ P (f z): thm

In this example, the simplifier performed a $\beta$-reduction in the first conjunct under the existential quantifier, and then did an “unwinding” or “one-point” reduction, recognising that the only possible value for the quantified variable x was the value z.

The second argument to SIMP_CONV is a list of theorems to be added to the provided simpset, and used as additional rewrite rules. In this way, users can temporarily augment standard simpsets with their own rewrites. If a particular set of theorems is often used as such an argument, then it is possible to build a simpset value to embody these new rewrites.

For example, the rewrite arithmeticTheory.LEFT_ADD_DISTRIB, which states that $p(m + n) = pm + pn$ is not part of any of HOL’s standard simpsets. This is because it can cause an unappealing increase in term size (there are two occurrences of $p$ on the right hand side of the theorem). Nonetheless, it is clear that this theorem may be appropriate on occasion:

> open arithmeticTheory;   ... output elided ...
> SIMP_CONV bossLib.arith_ss [LEFT_ADD_DISTRIB] ``p * (n + 1)``;
val it = ⊢ p * (n + 1) = p + n * p: thm

Note how the arith_ss simpset has not only simplified the intermediate (p * 1) term, but also re-ordered the addition to put the simpler term on the left, and sorted the multiplication’s arguments.

High-Level Simplification Tactics

The simplifier is implemented around the conversion SIMP_CONV introduced above. This is a function for “converting” terms into theorems. To apply the simplifier to goals (alternatively, to perform tactic-based proofs with the simplifier), HOL provides a number of tactics, all of which are available in bossLib. These tactics divide into two classes.

The first and more commonly used class is of high-level tactics used in the form $$ \mathtt{tactic\_name}[\mathit{th}_1,\dots,\mathit{th}_n] $$ where the various $\mathit{th}_i$ are theorems that are passed to the simplifier and used as additional rewrites. There is a simpset used here, but it is implicit: the global, “stateful” simpset, embodying all of a theory and a theory’s ancestors’ useful simplification technology. For more on the stateful simpset, see Section 8.5.3.5 below.

The commonly used tactics of this sort are simp, rw, fs and gs. All of these tactics use the stateful simpset, and (by default), add the arithmetic decision procedure for $\mathbb{N}$, as well as a handling of let-terms that turns let x = v in M into M with free occurrences of x replaced by v.

The exact behaviour of these tactics can be further adjusted with the use of special theorem forms, described below in Section 8.5.6.4.

simp : thm list -> tactic

A call to simp[$\mathit{th}_1,\dots,\mathit{th}_n$] simplifies the current goal, using the augmented stateful simpset described above, as well as the theorems passed in the argument list. Finally, all of the goal’s assumptions are also used as a source of rewrites. When an assumption is used by simp, it is converted into rewrite rules in the same way as theorems passed in the list given as the tactic’s argument. For example, an assumption ~P will be treated as the rewrite |- P = F.

> g ‘x < 3 /\ P x ==> x < 20 DIV 2’;   ... output elided ...
> e (simp[]);
OK..
val it =
   Initial goal proved.
   ⊢ x < 3 ∧ P x ⇒ x < 20 DIV 2: proof

The simp tactic is implemented with the low-level tactic asm_simp_tac (described below).

rw : thm list -> tactic

A call to rw[$\mathit{th}_1,\dots,\mathit{th}_n$] is similar in behaviour to simp with the same arguments but does its simplification while interleaving phases of aggressive “goal-stripping”. In particular, rw begins by stripping all outermost universal quantifiers and conjunctions. It follows this with elimination of variables v that appear in assumptions of the form v = e or e = v (where v cannot be free in e). After a phase of simplification (as per simp), the rw tactic then does a case-split on all free if-then-else subterms within the goal, strips away universal quantifiers, implications and conjunctions (à la STRIP_TAC), and then simplifies equalities involving data type constructors in the assumptions and goal. (Such equalities will simplify to falsity if the constructors are different, or will simplify with an injectivity result.)

This last phase of stripping may result in a goal that could simplify yet further but there is no final simplification to catch this possibility. Despite peculiarities such as this, rw is often a useful way to simplify and remove unnecessary propositional structure.

The rw tactic performs the same mixture of simplification and goal-splitting as the low-level tactic rw_tac.

gs : thm list -> tactic

The gs tactic (where “gs” should be read as “global simplification”) simplifies both the assumptions of a goal as well as its conclusion. The assumptions are repeatedly simplified with respect to each other, meaning that the process begins by simplifying the oldest assumption with all the other (newer) assumptions available as possible rewrites. Then, the next oldest assumption is simplified, using all the other assumptions (including the just-simplified oldest assumption). This process passes through the list of all assumptions and then repeats if any of the assumptions changed. When no further change occurs among the assumptions, all of the assumptions are used to simplify the goal’s conclusion.

When an assumption $A_i$ is simplified, a theorem of the form $\vdash A_i \Leftrightarrow A_i'$ is produced. Then $A_i'$ is added to the goal as a new assumption, using the theorem-tactical strip_assume_tac. This latter will (recursively) split conjunctions into multiple assumptions (i.e., an assumption $p \land q$ will turn into two assumptions, $p$ and $q$), will cause a case-split if the assumption is a disjunction, and will choose fresh variable names to eliminate existential quantifiers. If this “stripping” is not desired, the gns variant of gs can be used (‘n’ for “no strip”).

It can often be useful to eliminate variable equalities among assumptions (as is done by rw above). If this behaviour is also desired, the gvs variant can be used. The gnvs tactic, which combines both options, is also available.

Finally, the rgs variant sweeps through the assumption list in the opposite order, simplifying the newest assumption first.

fs : thm list -> tactic

The fs tactic is similar to gs in that it simplifies not only a goal’s conclusion but its assumptions as well.

It simplifies each assumption in turn, additionally using earlier assumptions in the simplification of later assumptions. After being simplified, each assumption is added back into the goal’s assumption list with the strip_assume_tac theorem-tactical.

fs attacks the assumptions in the order in which they appear in the list of terms that represent the goal’s assumptions. Typically then, the first assumption to be simplified will be the assumption most recently added. Viewed in the light of goalstackLib’s printing of goals, FULL_SIMP_TAC works its way up the list of assumptions, from bottom to top.

Unlike gs, the fs tactic makes exactly one pass over the assumptions before proceeding to simplify the goal. Though this is in principle more efficient, this comes at the cost of frequently being annoying to use. As with gs, there is an rfs variant to this tactic, which simplifies the goal’s assumptions in reverse order (but again, only once).

The fs tactic is based on the low-level tactic full_simp_tac.

Low-Level Simplification Tactics

The second class of simplification tactics are more primitive and explicitly take a simpset as an argument. This ability to specify a simpset to use allows for finer-grained control of just what the tactic will do. In particular, the bool_ss simpset is relatively common as an argument to these tactics precisely because it does so little.

All these tactics have upper-case aliases (e.g., SIMP_TAC and simp_tac are the same function). It is up to the user to decide which they prefer to see in their proof scripts.

simp_tac : simpset -> thm list -> tactic

The tactic simp_tac is the simplest simplification function: it attempts to simplify the current goal (ignoring the assumptions) using the given simpset and the additional theorems. It is little more than the lifting of the underlying SIMP_CONV conversion to the tactic level through the use of the standard function CONV_TAC.

asm_simp_tac : simpset -> thm list -> tactic

Like simp_tac, asm_simp_tac simplifies the current goal (leaving the assumptions untouched), but includes the goal’s assumptions as extra rewrite rules. Thus:

OK..
1 subgoal:
val it =
   
    0.  x = 3
   ------------------------------------
        P x

> e (asm_simp_tac bool_ss []);
OK..
1 subgoal:
val it =
   
    0.  x = 3
   ------------------------------------
        P 3

In this example, asm_simp_tac used x = 3 as an additional rewrite rule, and replaced the x of P x with 3. When an assumption is used by asm_simp_tac it is converted into rewrite rules in the same way as theorems passed in the list given as the tactic’s second argument. For example, an assumption ~P will be treated as the rewrite |- P = F.

full_simp_tac : simpset -> thm list -> tactic

The tactic full_simp_tac simplifies not only a goal’s conclusion but its assumptions as well. It proceeds by simplifying each assumption in turn, additionally using earlier assumptions in the simplification of later assumptions. After being simplified, each assumption is added back into the goal’s assumption list with the strip_assume_tac theorem-tactical. This means that assumptions that become conjunctions will have each conjunct assumed separately. Assumptions that become disjunctions will cause one new sub-goal to be created for each disjunct. If an assumption is simplified to false, this will solve the goal.

The full_simp_tac tactic attacks the assumptions in the order in which they appear in the list of terms that represent the goal’s assumptions. Typically then, the first assumption to be simplified will be the assumption most recently added. Viewed in the light of goalstackLib’s printing of goals, full_simp_tac works its way up the list of assumptions, from bottom to top.

The following demonstrates a simple use of full_simp_tac:

OK..
1 subgoal:
val it =
   
    0.  f x < 10
    1.  x = 4
   ------------------------------------
        4 + x < 10

> e (full_simp_tac bool_ss []);
OK..
1 subgoal:
val it =
   
    0.  f 4 < 10
    1.  x = 4
   ------------------------------------
        4 + 4 < 10

In this example, the assumption x = 4 caused the x in the assumption f x < 10 to be replaced by 4. The x in the goal was similarly replaced. If the assumptions had appeared in the opposite order, only the x of the goal would have changed.

The next session demonstrates more interesting behaviour:

OK..
1 subgoal:
val it =
   
    0.  x ≤ 4
   ------------------------------------
        f x + 1 < 10

> e (full_simp_tac bool_ss [arithmeticTheory.LESS_OR_EQ]);
OK..
2 subgoals:
val it =
   
    0.  x = 4
   ------------------------------------
        f 4 + 1 < 10
   
    0.  x < 4
   ------------------------------------
        f x + 1 < 10

In this example, the goal was rewritten with the theorem stating $$ \vdash x \leq y \iff x < y \lor x = y $$ Turning the assumption into a disjunction resulted in two sub-goals. In the second of these, the assumption x = 4 further simplified the rest of the goal.

rw_tac : simpset -> thm list -> tactic

Though its type is the same as the simplification tactics already described, rw_tac is “augmented” in two ways:

  • When simplifying the goal, the provided simpset is augmented not only with the theorems explicitly passed in the second argument, but also with all of the rewrite rules from the TypeBase, as well as with the goal’s assumptions. These rewrites include results about record field selectors and updators, as well as distinctness and injectivity theorems for data type constructors.
  • rw_tac also repeatedly “strips” the goal in the same way as the high-level rw tactic (see above).

The augmentation of the provided simpset that occurs before rw_tac does any simplification work can be slow when the TypeBase is large.

The standard simpsets

HOL comes with a number of standard simpsets. All of these are accessible from within bossLib, though some originate in other structures.

pure_ss and bool_ss

The pure_ss simpset (defined in structure pureSimps) contains no rewrite theorems at all, and plays the role of a blank slate within the space of possible simpsets. When constructing a completely new simpset, pure_ss is a possible starting point. The pure_ss simpset has just two components: congruence rules for specifying how to traverse terms, and a function that turns theorems into rewrite rules. Congruence rules are further described in Section 8.5.6; the generation of rewrite rules from theorems is described in Section 8.5.5.3.

The bool_ss simpset (defined in structure boolSimps) is often used when other simpsets might do too much. It contains rewrite rules for the boolean connectives, and little more. It contains all of the de Morgan theorems for moving negations in over the connectives (conjunction, disjunction, implication and conditional expressions), including the quantifier rules that have $\neg(\forall x.\,P(x))$ and $\neg(\exists x.\,P(x))$ on their left-hand sides. It also contains the rules specifying the behaviour of the connectives when the constants T and F appear as their arguments. (One such rule is |- T /\ p = p.)

As in the example above, bool_ss also performs $\beta$-reductions and one-point unwindings. The latter turns terms of the form $$ \exists x.\;P(x)\land\dots x = e \dots\land Q(x) $$ into $$ P(e) \land \dots \land Q(e) $$ Similarly, unwinding will turn $\forall x.\;x = e \Rightarrow P(x)$ into $P(e)$.

Finally, bool_ss also includes congruence rules that allow the simplifier to make additional assumptions when simplifying implications and conditional expressions. This feature is further explained in Section 8.5.5 below, but can be illustrated by some examples (the first also demonstrates unwinding under a universal quantifier):

> SIMP_CONV bool_ss [] “!x. (x = 3) /\ P x ==> Q x /\ P 3”;
val it = ⊢ (∀x. x = 3 ∧ P x ⇒ Q x ∧ P 3) ⇔ P 3 ⇒ Q 3: thm

> SIMP_CONV bool_ss [] “if x <> 3 then P x else Q x”;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ (if x ≠ 3 then P x else Q x) = if x ≠ 3 then P x else Q 3: thm

std_ss

The std_ss simpset is defined in bossLib, and adds rewrite rules pertinent to the types of sums, pairs, options and natural numbers to bool_ss.

> SIMP_CONV std_ss [] “FST (x,y) + OUTR (INR z)”;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = ⊢ FST (x,y) + OUTR (INR z) = x + z: thm

> SIMP_CONV std_ss [] “case SOME x of NONE => P | SOME y => f y”;
<<HOL message: inventing new type variable names: 'a, 'b>>
val it = ⊢ (case SOME x of NONE => P | SOME y => f y) = f x: thm

With the natural numbers, the std_ss simpset can calculate with ground values, and also includes a suite of “obvious rewrites” for formulas including variables.

> SIMP_CONV std_ss [] “P (0 <= x) /\ Q (y + x - y)”;
val it = ⊢ P (0 ≤ x) ∧ Q (y + x − y) ⇔ P T ∧ Q x: thm

> SIMP_CONV std_ss [] “23 * 6 + 7 ** 2 - 31 DIV 3”;
val it = ⊢ 23 * 6 + 7² − 31 DIV 3 = 177: thm

arith_ss

The arith_ss simpset (defined in bossLib) extends std_ss by adding the ability to decide formulas of Presburger arithmetic, and to normalise arithmetic expressions (collecting coefficients, and re-ordering summands). The underlying natural number decision procedure is that described in Section 8.8 below.

These two facets of the arith_ss simpset are demonstrated here:

> SIMP_CONV arith_ss [] ``x < 3 /\ P x ==> x < 20 DIV 2``;
val it = ⊢ x < 3 ∧ P x ⇒ x < 20 DIV 2 ⇔ T: thm

> SIMP_CONV arith_ss [] ``2 * x + y - x + y``;
val it = ⊢ 2 * x + y − x + y = x + 2 * y: thm

Note that subtraction over the natural numbers works in ways that can seem unintuitive. In particular, coefficient normalisation may not occur when first expected:

> SIMP_CONV arith_ss [] ``2 * x + y - z + y``;
Exception- UNCHANGED raised

Over the natural numbers, the expression $2 x + y - z + y$ is not equal to $2 x + 2 y - z$. In particular, these expressions are not equal when $2x + y < z$.

list_ss

The last pure simpset value in bossLib, list_ss adds rewrite theorems about the type of lists to arith_ss. These rewrites include the obvious facts about the list type’s constructors NIL and CONS, such as the fact that CONS is injective:

   (h1 :: t1 = h2 :: t2) = (h1 = h2) /\ (t1 = t2)

Conveniently, list_ss also includes rewrites for the functions defined by primitive recursion over lists. Examples include MAP, FILTER and LENGTH. Thus:

> SIMP_CONV list_ss [] ``MAP (\x. x + 1) [1;2;3;4]``;
val it = ⊢ MAP (λx. x + 1) [1; 2; 3; 4] = [2; 3; 4; 5]: thm

> SIMP_CONV list_ss [] ``FILTER (\x. x < 4) [1;2;y + 4]``;
val it = ⊢ FILTER (λx. x < 4) [1; 2; y + 4] = [1; 2]: thm

> SIMP_CONV list_ss [] ``LENGTH (FILTER ODD [1;2;3;4;5])``;
val it = ⊢ LENGTH (FILTER ODD [1; 2; 3; 4; 5]) = 3: thm

These examples demonstrate how the simplifier can be used as a general purpose symbolic evaluator for terms that look a great deal like those that appear in a functional programming language. Note that this functionality is also provided by computeLib (see Section 8.6 below); computeLib is more efficient, but less general than the simplifier. For example:

> EVAL ``FILTER (\x. x < 4) [1;2;y + 4]``;
val it =
   ⊢ FILTER (λx. x < 4) [1; 2; y + 4] =
     1::2::if y + 4 < 4 then [y + 4] else []: thm

The "stateful" simpsetsrw_ss()

The last simpset exported by bossLib is hidden behind a function. The srw_ss value has type unit -> simpset, so that one must type srw_ss() in order to get a simpset value. This use of a function type allows the underlying simpset to be stored in an SML reference, and allows it to be updated dynamically. In this way, referential transparency is deliberately broken. All of the other simpsets will always behave identically: SIMP_CONV\ bool_ss is the same simplification routine wherever and whenever it is called.

In contrast, srw_ss is designed to be updated. When a theory is loaded, when a new type is defined, the value behind srw_ss() changes, and the behaviour of SIMP_CONV applied to (srw_ss()) changes with it. The design philosophy behind srw_ss is that it should always be a reasonable first choice in all situations where the simplifier is used.

This versatility is illustrated in the following example:

> Datatype: tree = Leaf | Node num tree tree
  End
<<HOL warning: Datatype.Hol_datatype: Constructor "Leaf" in new type "tree"
               duplicates constructor in type "btree", which will be
               invalidated by this definition>>
<<HOL warning: Datatype.Hol_datatype: Constructor "Node" in new type "tree"
               duplicates constructor in type "btree", which will be
               invalidated by this definition>>
<<HOL message: Defined type: "tree">>

> SIMP_CONV (srw_ss()) [] “Node x Leaf Leaf = Node 3 t1 t2”;
val it = ⊢ Node x Leaf Leaf = Node 3 t1 t2 ⇔ x = 3 ∧ t1 = Leaf ∧ t2 = Leaf:
   thm

> load "pred_setTheory";
val it = (): unit

> SIMP_CONV (srw_ss()) [] “x IN { y | y < 6}”;
val it = ⊢ x ∈ {y | y < 6} ⇔ x < 6: thm

Users can augment the stateful simpset themselves with the function

   BasicProvers.export_rewrites : string list -> unit

The strings passed to export_rewrites are the names of theorems in the current segment (those that will be exported when export_theory is called). Not only are these theorems added to the underlying simpset in the current session, but they will be added in future sessions when the current theory is reloaded.

> Definition tsize_def:
    (tsize Leaf = 0) /\
    (tsize (Node n t1 t2) = n + tsize t1 + tsize t2)
  End
Definition has been stored under "tsize_def"
val tsize_def =
   ⊢ tsize Leaf = 0 ∧
     ∀n t1 t2. tsize (Node n t1 t2) = n + tsize t1 + tsize t2: thm

> val _ = BasicProvers.export_rewrites ["tsize_def"];

> SIMP_CONV (srw_ss()) [] ``tsize (Node 4 (Node 6 Leaf Leaf) Leaf)``;
val it = ⊢ tsize (Node 4 (Node 6 Leaf Leaf) Leaf) = 10: thm

Alternatively, the user may also flag theorems directly when using store_thm, save_thm, or the Theorem and Definition syntaxes by appending the simp attribute to the name of the theorem. Thus:

Theorem useful_rwt[simp]:
  ...term...
Proof ...tactic...
QED

is a way of avoiding having to write a call to export_rewrites. Equally, the example above could be written:

> Definition tsize_def[simp]:
    (tsize Leaf = 0) /\
    (tsize (Node n t1 t2) = n + tsize t1 + tsize t2)
  End   ... output elided ...

As a general rule, (srw_ss()) includes all of its context’s “obvious rewrites”, as well as code to do standard calculations (such as the arithmetic performed in the above example). It does not include decision procedures that may exhibit occasional poor performance, so the simpset fragments containing these procedures should be added manually to those simplification invocations that need them.

Simpset fragments

The simpset fragment is the basic building block that is used to construct simpset values. There is one basic function that performs this construction:

   op ++  : simpset * ssfrag -> simpset

where ++ is an infix. In general, it is best to build on top of the pure_ss simpset or one of its descendants in order to pick up the default “filter” function for converting theorems to rewrite rules. (This filtering process is described below in Section 8.5.5.3.)

A simpset also carries a (usually empty) set of excluded fragment names, set up by simpLib.exclude_ssfrags (which is what Proof[exclude_frags = \ldots] ultimately calls). When the incoming fragment of ss\ ++\ frag has a name in that set, the addition is a silent no-op and ss is returned unchanged. Use simpLib.force_add, or the SF marker in a thm-list argument to a simplification tactic, to override that prohibition for a particular fragment.

For major theories (or groups thereof), a collection of relevant simpset fragments is usually found in the module <thy>Simps, with <thy> the name of the theory. For example, simpset fragments for the theory of natural numbers are found in numSimps, and fragments for lists are found in listSimps.

Some of the distribution’s standard simpset fragments are described in Table 8.5.4. These and other simpset fragments are described in more detail in the REFERENCE.

Table 8.5.4. Some of HOL’s standard simpset fragments.

FragmentDescription
ARITH_ssEmbodies decision procedure for universal Presburger arithmetic over $\mathbb{N}$. (Defined in numSimps; used in high-level simplification tactics.)
BOOL_ssStandard rewrites for the boolean operators (conjunction, negation etc.), and conversion for performing $\beta$-reduction. (Defined in boolSimps; part of bool_ss.)
CONG_ssCongruence rules for implication and conditional expressions. (Defined in boolSimps; part of bool_ss.)
CONJ_ssLets conjuncts be used as assumptions when rewriting other conjuncts. If simplifying $c_1 \land c_2$, $c_2$ is assumed while $c_1$ is simplified to $c_1'$. Then $c_1'$ is assumed while $c_2$ is simplified. (Defined in boolSimps.)
DNF_ssNormalises to disjunctive normal form; quantifiers moved for elimination over equalities. (Defined in boolSimps.)
ETA_ssEliminates eta-redexes, i.e., terms of form $(\lambda x.\;M\;x)$ with $x$ not free in $M$. (Defined in boolSimps.)

Simpset fragments are ultimately constructed with the SSFRAG constructor:

   SSFRAG : {
     convs  : convdata list,
     rewrs  : thm list,
     ac     : (thm * thm) list,
     filter : (controlled_thm -> controlled_thm list) option,
     dprocs : Traverse.reducer list,
     congs  : thm list,
     name   : string option
   } -> ssfrag

A complete description of the various fields of the record passed to SSFRAG, and their meaning is given in REFERENCE. The rewrites function provides an easy route to constructing a fragment that just includes a list of rewrites:

   rewrites : thm list -> ssfrag

Removing rewrites and conversions from simpsets

The -* (infix) function can be used to remove elements from simpsets. This can be done to temporarily affect the simplifier when it is applied to a particular goal. For example:

> SIMP_CONV (srw_ss()) [] “x ++ (y ++ z)”;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ x ⧺ (y ⧺ z) = x ⧺ y ⧺ z: thm

> SIMP_CONV (srw_ss() -* ["APPEND_ASSOC"]) [] “x ++ (y ++ z)”;
Exception- <<HOL message: inventing new type variable names: 'a>>
UNCHANGED raised

The second argument to -* is a list of strings, naming rewrite theorems, conversions or decision procedures. The names to use are visible if simpset values are printed out in the interactive session. The example below demonstrates removing beta-conversion:

> SIMP_CONV (bool_ss -* ["BETA_CONV"]) [] “(\x. x + 3) 10”;
Exception- UNCHANGED raised

Further, because a theorem like AND_CLAUSES

AND_CLAUSES
  ⊢ ∀t. (T ∧ t ⇔ t) ∧ (t ∧ T ⇔ t) ∧ (F ∧ t ⇔ F) ∧
         (t ∧ F ⇔ F) ∧ (t ∧ t ⇔ t)

has multiple conjuncts, one theorem can generate multiple different rewrites. Specific sub-rewrites can be removed from a simpset without affecting others derived from the same original theorem by appending numbers to the theorem name:

> SIMP_CONV (bool_ss -* ["AND_CLAUSES"]) [] “(T ∧ p) ∧ (q ∧ T)”;
Exception- UNCHANGED raised

> SIMP_CONV (bool_ss -* ["AND_CLAUSES.1"]) [] “(T ∧ p) ∧ (q ∧ T)”;
val it = ⊢ (T ∧ p) ∧ q ∧ T ⇔ (T ∧ p) ∧ q: thm

If using a high-level tactic such as simp, there is no simpset value visible to modify with -*. Instead, one must use a special theorem form (see Section 8.5.6.4 below), Excl to exclude a rewrite. For example, sometimes the associativity of list-append can be annoying (here it masks the rewrite defining list-append):

> g `f (x ++ (h::t ++ y)) = f (x ++ h::(t ++ y))`;   ... output elided ...
> e (simp[]);
OK..
1 subgoal:
val it =
   
   f (x ⧺ h::t ⧺ y) = f (x ⧺ h::(t ⧺ y))

We can prevent the application of this normalising rewrite with the Excl form:

> b();   ... output elided ...
> e (simp[Excl "APPEND_ASSOC"]);
OK..
val it =
   Initial goal proved.
   ⊢ f (x ⧺ (h::t ⧺ y)) = f (x ⧺ h::(t ⧺ y)): proof

One can exclude whole simpset fragments from the high-level tactics with the special ExclSF form, which also takes a string argument. This string is the name of the fragment to be removed, where by convention fragments have the same name as their SML identifier with the _ss suffix removed. Thus, one can use high-level tactics with the arithmetic decision procedure removed:

> g ‘x < 5*2 ==> x < 13’;   ... output elided ...
> e (simp[ExclSF "ARITH"]);
OK..
1 subgoal:
val it =
   
   x < 10 ⇒ x < 13

The exclusion that ExclSF effects is local to the simp call in which it appears. For an exclusion that persists across all the simplification tactics in a proof body, use the Proof[exclude_frags\ =\ \ldots] attribute (the simpLib.exclude_ssfrags function it relies on records the names in the simpset’s excluded set, so subsequent uses of ++ also respect it; see Section 8.5.4). Inside such a proof body, the SF marker can opt back in for an individual simp call.

Rewriting with the simplifier

Rewriting is the simplifier’s “core operation”. This section describes the action of rewriting in more detail.

Basic rewriting

Given a rewrite rule of the form $$ \vdash \ell = r $$ the simplifier will perform a top-down scan of the input term $t$, looking for matches (see Section 8.5.5.4 below) of $\ell$ inside $t$. This match will occur at a sub-term of $t$ (call it $t_0$) and will return an instantiation. When this instantiation is applied to the rewrite rule, the result will be a new equation of the form $$ \vdash t_0 = r' $$ Because the system then has a theorem expressing an equivalence for $t_0$ it can create the new equation $$ \vdash \underbrace{(\dots t_0\dots)}_t = (\dots r' \dots) $$ The traversal of the term to be simplified is repeated until no further matches for the simplifier’s rewrite rules are found. The traversal strategy is

  1. While there are any matches for stored rewrite rules at this level, continue to apply them. The order in which rewrite rules are applied can not be relied on, except that when a simpset includes two rewrites with exactly the same left-hand sides, the rewrite added later will get matched in preference. (This allows a certain amount of rewrite-overloading in the construction of simpsets.)
  2. Recurse into the term’s sub-terms. The way in which terms are traversed at this step can be controlled by congruence rules (an advanced feature, see Section 8.5.6.1 below).
  3. If step 8.5.5.1 changed the term at all, try another phase of rewriting at this level. If this fails, or if there was no change from the traversal of the sub-terms, try any embedded decision procedures (see Section 8.5.6.3). If the rewriting phase or any of the decision procedures altered the term, return to step 8.5.5.1. Otherwise, finish.

Conditional rewriting

The above description is a slight simplification of the true state of affairs. One particularly powerful feature of the simplifier is that it really uses conditional rewrite rules. These are theorems of the form $$ \vdash P \Rightarrow (\ell = r) $$ When the simplifier finds a match for term $\ell$ during its traversal of the term, it attempts to discharge the condition $P$. If the simplifier can simplify the term $P$ to truth, then the instance of $\ell$ in the term being traversed can be replaced by the appropriate instantiation of $r$.

When simplifying $P$ (a term that does not necessarily even occur in the original), the simplifier may find itself applying another conditional rewrite rule. In order to stop excessive recursive applications, the simplifier keeps track of a stack of all the side-conditions it is working on. The simplifier will give up on side-condition proving if it notices a repetition in this stack. There is also a user-accessible variable, Cond_rewr.stack_limit which specifies the maximum size of stack the simplifier is allowed to use.

Conditional rewrites can be extremely useful. For example, theorems about division and modulus are frequently accompanied by conditions requiring the divisor to be non-zero. The simplifier can often discharge these, particularly if it includes an arithmetic decision procedure. For example, the theorem MOD_MOD from the theory arithmetic states $$ \vdash 0 < n \;\Rightarrow \; (k\,\textsf{MOD}\,n)\,\textsf{MOD}\,n = k \,\textsf{MOD}\,n $$ The simplifier (specifically, SIMP_CONV\ arith_ss\ [MOD_MOD]) can use this theorem to simplify the term (k MOD (x + 1)) MOD (x + 1): the arithmetic decision procedure can prove that 0 < x + 1, justifying the rewrite.

Though conditional rewrites are powerful, not every theorem of the form described above is an appropriate choice. A badly chosen rewrite may cause the simplifier’s performance to degrade considerably, as it wastes time attempting to prove impossible side-conditions. For example, the simplifier is not very good at finding existential witnesses. This means that the conditional rewrite $$ \vdash x < y \land y < z \Rightarrow (x < z = \top) $$ will not work as one might hope. In general, the simplifier is not a good tool for performing transitivity reasoning. (Try first-order tools such as PROVE_TAC instead.)

Generating rewrite rules from theorems

There are two routes by which a theorem for rewriting can be passed to the simplifier: either as an explicit argument to one of the SML functions (SIMP_CONV, ASM_SIMP_TAC etc.) that take theorem lists as arguments, or by being included in a simpset fragment which is merged into a simpset. In both cases, these theorems are transformed before being used. The transformations applied are designed to make interactive use as convenient as possible.

In particular, it is not necessary to pass the simplifier theorems that are exactly of the form $$ \vdash P \Rightarrow (\ell = r) $$ Instead, the simplifier will transform its input theorems to extract rewrites of this form itself. The exact transformation performed is dependent on the simpset being used: each simpset contains its own “filter” function which is applied to theorems that are added to it. Most simpsets use the filter function from the pure_ss simpset (see Section 8.5.3.1). However, when a simpset fragment is added to a full simpset, the fragment can specify an additional filter component. If specified, this function is of type controlled_thm\ ->\ controlled_thm\ list, and is applied to each of the theorems produced by the existing simpset’s filter. (A “controlled” theorem is one that is accompanied by a piece of “control” data expressing the limit (if any) on the number of times it can be applied. See Section 8.5.6.4 for how users can introduce these limits. The “control” type appears in the SML module BoundedRewrites.)

The rewrite-producing filter in pure_ss strips away conjunctions, implications and universal quantifications until it has either an equality theorem, or some other boolean form. For example, the theorem ADD_MODULUS states $$ \vdash \begin{array}{l} (\forall n\;x.\;\;0 < n \Rightarrow ((x + n)\,\textsf{MOD}\,n = x\,\textsf{MOD}\,n)) \;\;\land\\ (\forall n\;x.\;\;0 < n \Rightarrow ((n + x)\,\textsf{MOD}\,n = x\,\textsf{MOD}\,n)) \end{array} $$ This theorem becomes two rewrite rules $$ \begin{array}{l} \vdash 0 < n \Rightarrow ((x + n)\,\textsf{MOD}\,n = x\,\textsf{MOD}\,n)\\ \vdash 0 < n \Rightarrow ((n + x)\,\textsf{MOD}\,n = x\,\textsf{MOD}\,n) \end{array} $$

If looking at an equality where there are variables on the right-hand side that do not occur on the left-hand side, the simplifier transforms this to the rule $$ \vdash (\ell = r) = \top $$ Similarly, if a boolean negation $\neg P$, becomes the rule $$ \vdash P = \bot $$ and other boolean formulas $P$ become $$ \vdash P = \top $$

Finally, if looking at an equality whose left-hand side is itself an equality, and where the right-hand side is not an equality as well, the simplifier transforms $(x = y) = P$ into the two rules $$ \begin{array}{l} \vdash (x = y) = P\\ \vdash (y = x) = P \end{array} $$ This is generally useful. For example, a theorem such as $$ \vdash \neg(\textsf{SUC}\,n = 0) $$ will cause the simplifier to rewrite both $(\textsf{SUC}\,n = 0)$ and $(0 = \textsf{SUC}\,n)$ to false.

The restriction that the right-hand side of such a rule not itself be an equality is a simple heuristic that prevents some forms of looping.

Matching rewrite rules

Given a rewrite theorem $\vdash \ell = r$, the first stage of performing a rewrite is determining whether or not $\ell$ can be instantiated so as to make it equal to the term that is being rewritten. This process is known as matching. For example, if $\ell$ is the term $\textsf{SUC}(n)$, then matching it against the term $\textsf{SUC}(3)$ will succeed, and find the instantiation $n\mapsto 3$. In contrast with unification, matching is not symmetrical: a pattern $\textsf{SUC}(3)$ will not match the term $\textsf{SUC}(n)$.

The simplifier uses a special form of higher-order matching. If a pattern includes a variable of some function type ($f$ say), and that variable is applied to an argument $a$ that includes no variables except those that are bound by an abstraction at a higher scope, then the combined term $f(a)$ will match any term of the appropriate type as long as the only occurrences of the bound variables in $a$ are in sub-terms matching $a$.

Assume for the following examples that the variable $x$ is bound at a higher scope. Then, if $f(x)$ is to match $x + 4$, the variable $f$ will be instantiated to $(\lambda x.\; x + 4)$. If $f(x)$ is to match $3 + z$, then $f$ will be instantiated to $(\lambda x.\;3 + z)$. Further $f(x + 1)$ matches $x + 1 < 7$, but does not match $x + 2 < 7$.

Higher-order matching of this sort makes it easy to express quantifier movement results as rewrite rules, and have these rules applied by the simplifier. For example, the theorem $$ \vdash (\exists x. \;P(x)\lor Q(x)) = (\exists x.\;P(x)) \lor (\exists x.\;Q(x)) $$ has two variables of a function-type ($P$ and $Q$), and both are applied to the bound variable $x$. This means that when applied to the input $$ \exists z. \;z < 4 \lor z + x = 5 * z $$ the matcher will find the instantiation $$ \begin{array}{l} P \mapsto (\lambda z.\;z < 4)\\ Q \mapsto (\lambda z.\;z + x = 5 * z) \end{array} $$

Performing this instantiation, and then doing some $\beta$-reduction on the rewrite rule, produces the theorem $$ \vdash (\exists z. \;z < 4 \lor z + x = 5 * z) = (\exists z. \;z < 4) \lor (\exists z.\;z + x = 5 * z) $$ as required.

Another example of a rule that the simplifier will use successfully is $$ \vdash f \circ (\lambda x.\; g(x)) = (\lambda x.\;f(g(x))) $$ The presence of the abstraction on the left-hand side of the rule requires an abstraction to appear in the term to be matched, so this rule can be seen as an implementation of a method to move abstractions up over function compositions.

An example of a possible left-hand side that will not match as generally as might be liked is $(\exists x.\;P(x + y))$. This is because the predicate $P$ is applied to an argument that includes the free variable $y$.

Advanced features

This section describes some of the simplifier’s advanced features.

Congruence rules

Congruence rules control the way the simplifier traverses a term. They also provide a mechanism by which additional assumptions can be added to the simplifier’s context, representing information about the containing context. The simplest congruence rules are built into the pure_ss simpset. They specify how to traverse application and abstraction terms. At this fundamental level, these congruence rules are little more than the rules of inference ABS $$ \frac{\Gamma \vdash t_1 = t_2} {\Gamma \vdash (\lambda x.\;t_1) = (\lambda x.\;t_2)} $$ (where $x\not\in\Gamma$) and MK_COMB $$ \frac{\Gamma \vdash f = g \qquad \qquad \Delta \vdash x = y} {\Gamma \cup \Delta \vdash f(x) = g(y)} $$ When specifying the action of the simplifier, these rules should be read upwards. With ABS, for example, the rule says “when simplifying an abstraction, simplify the body $t_1$ to some new $t_2$, and then the result is formed by re-abstracting with the bound variable $x$.”

Further congruence rules should be added to the simplifier in the form of theorems, via the congs field of the records passed to the SSFRAG constructor. Such congruence rules should be of the form $$ \mathit{cond_1} \Rightarrow \mathit{cond_2} \Rightarrow \dots (E_1 = E_2) $$ where $E_1$ is the form to be rewritten. Each $\mathit{cond}_i$ can either be an arbitrary boolean formula (in which case it is treated as a side-condition to be discharged) or an equation of the general form $$ \forall \vec{v}. \;\mathit{ctxt}_1 \Rightarrow \mathit{ctxt}_2 \Rightarrow \dots (V_1(\vec{v}) = V_2(\vec{v})) $$ where the variable $V_2$ must occur free in $E_2$.

For example, the theorem form of MK_COMB would be $$ \vdash (f = g) \Rightarrow (x = y) \Rightarrow (f(x) = g(y)) $$ and the theorem form of ABS would be $$ \vdash (\forall x. \;f (x) = g (x)) \Rightarrow (\lambda x. \;f(x)) = (\lambda x.\;g(x)) $$ The form for ABS demonstrates how it is possible for congruence rules to handle bound variables. Because the congruence rules are matched with the higher-order match of Section 8.5.5.4, this rule will match all possible abstraction terms.

These simple examples have not yet demonstrated the use of $\mathit{ctxt}$ conditions on sub-equations. An example of this is the congruence rule (found in CONG_ss) for implications. This states $$ \vdash (P = P') \Rightarrow (P' \Rightarrow (Q = Q')) \Rightarrow (P \Rightarrow Q = P' \Rightarrow Q') $$ This rule should be read: “When simplifying $P\Rightarrow Q$, first simplify $P$ to $P'$. Then assume $P'$, and simplify $Q$ to $Q'$. Then the result is $P' \Rightarrow Q'$.”

The rule for conditional expressions is $$ \vdash \begin{array}{l} (P = P') \Rightarrow (P' \Rightarrow (x = x')) \Rightarrow (\neg P' \Rightarrow (y = y')) \;\Rightarrow\\ (\textsf{if}\;P\;\textsf{then}\;x\;\textsf{else}\;y = \textsf{if}\;P'\;\textsf{then}\;x'\;\textsf{else}\;y') \end{array} $$ This rule allows the guard to be assumed when simplifying the true-branch of the conditional, and its negation to be assumed when simplifying the false-branch.

The contextual assumptions from congruence rules are turned into rewrites using the mechanisms described in Section 8.5.5.3.

Congruence rules can be used to achieve a number of interesting effects. For example, a congruence can specify that sub-terms not be simplified if desired. This might be used to prevent simplification of the branches of conditional expressions: $$ \vdash (P = P') \Rightarrow (\textsf{if}\;P\;\textsf{then}\;x\;\textsf{else}\;y = \textsf{if}\;P'\;\textsf{then}\;x\;\textsf{else}\;y) $$ If added to the simplifier, this rule will take precedence over any other rules for conditional expressions (masking the one above from CONG_ss, say), and will cause the simplifier to only descend into the guard. With the standard rewrites (from BOOL_ss): $$ \begin{array}{l} \vdash \;\textsf{if}\;\top\;\textsf{then}\;x\;\textsf{else}\;y \,\;=\,\; x\\ \vdash \;\textsf{if}\;\bot\;\textsf{then}\;x\;\textsf{else}\;y \,\;=\,\; y \end{array} $$ users can choose to have the simplifier completely ignore a conditional’s branches until that conditional’s guard is simplified to either true or false.

As a convenience, congruence rules expressed in the format used by termination analysis in defining recursive functions (see Section 7.6.2.5), can also be passed to the simplifier.

AC-normalisation

The simplifier can be used to normalise terms involving associative and commutative constants. This process is known as AC-normalisation. The simplifier will perform AC-normalisation for those constants which have their associativity and commutativity theorems provided in a constituent simpset fragment’s ac field.

For example, the following simpset fragment will cause AC-normalisation of disjunctions

SSFRAG { name = NONE,
convs = [], rewrs = [], congs = [],
filter = NONE, ac = [(DISJ_ASSOC, DISJ_COMM)],
dprocs = [] }

The pair of provided theorems must state $$ \begin{array}{lcl} x \oplus y &=& y \oplus x\\ x \oplus (y \oplus z) &=& (x \oplus y) \oplus z \end{array} $$ for a constant $\oplus$. The theorems may be universally quantified, and the associativity theorem may be oriented either way. Further, either the associativity theorem or the commutativity theorem may be the first component of the pair. Assuming the simpset fragment above is bound to the SML identifier DISJ_ss, its behaviour is demonstrated in the following example:

> SIMP_CONV (bool_ss ++ DISJ_ss) [] ``p /\ q \/ r \/ P z``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ p ∧ q ∨ r ∨ P z ⇔ r ∨ P z ∨ p ∧ q: thm

The order of operands in the AC-normal form that the simplifer’s AC-normalisation works toward is unspecified. However, the normal form is always right-associated. Note also that the arith_ss simpset, and the ARITH_ss fragment which is its basis, have their own bespoke normalisation procedures for addition over the natural numbers. Mixing AC-normalisation, as described here, with arith_ss can cause the simplifier to go into an infinite loop.

AC theorems can also be added to simpsets via the theorem-list part of the tactic and conversion interface, using the special rewrite form AC:

> SIMP_CONV bool_ss [AC DISJ_ASSOC DISJ_COMM] ``p /\ q \/ r \/ P z``;
<<HOL message: inventing new type variable names: 'a>>
val it = ⊢ p ∧ q ∨ r ∨ P z ⇔ r ∨ P z ∨ p ∧ q: thm

See Section 8.5.6.4 for more on special rewrite forms.

Embedding code

The simplifier features two different ways in which user-code can be embedded into its traversal and simplification of input terms. By embedding their own code, users can customise the behaviour of the simplifier to a significant extent.

User conversions

The simpler of the two methods allows the simplifier to include user-supplied conversions. These are added to simpsets in the convs field of simpset fragments. This field takes lists of values of type

   { name: string,
    trace: int,
      key: (term list * term) option,
     conv: (term list -> term -> thm) -> term list -> term -> thm}

The name and trace fields are used when simplifier tracing is turned on. If the conversion is applied, and if the simplifier trace level is greater than or equal to the trace field, then a message about the conversion’s application (including its name) will be emitted.

The key field of the above record is used to specify the sub-terms to which the conversion should be applied. If the value is NONE, then the conversion will be tried at every position. Otherwise, the conversion is applied at term positions matching the provided pattern. The first component of the pattern is a list of variables that should be treated as constants when finding pattern matches. The second component is the term pattern itself. Matching against this component is not done by the higher-order match of Section 8.5.5.4, but by a higher-order “term-net”. This form of matching does not aim to be precise; it is used to efficiently eliminate clearly impossible matches. It does not check types, and does not check multiple bindings. This means that the conversion will not only be applied to terms that are exact matches for the supplied pattern.

Finally, the conversion itself. Most uses of this facility are to add normal HOL conversions (of type term->thm), and this can be done by ignoring the conv field’s first two parameters. For a conversion myconv, the standard idiom is to write K\ (K\ myconv). If the user desires, however, their code can refer to the first two parameters. The second parameter is the stack of side-conditions that have been attempted so far. The first enables the user’s code to call back to the simplifier, passing the stack of side-conditions, and a new side-condition to solve. The term argument must be of type :bool, and the recursive call will simplify it to true (and call EQT_ELIM to turn a term $t$ into the theorem $\vdash t$). This restriction is lifted for decision procedures (see below), but for conversions the recursive call can only be used for side-condition discharge. Note also that it is the user’s responsibility to pass an appropriately updated stack of side-conditions to the recursive invocation of the simplifier.

A user-supplied conversion should never return the reflexive identity (an instance of $\vdash t = t$). This will cause the simplifier to loop. Rather than return such a result, raise a HOL_ERR or Conv.UNCHANGED exception. (Both are treated the same by the simplifier.)

Context-aware decision procedures

Another, more involved, method for embedding user code into the simplifier is via the dprocs field of the simpset fragment structure. This method is more general than adding conversions, and also allows user code to construct and maintain its own bespoke logical contexts.

The dprocs field requires lists of values of the type Traverse.reducer. These values are constructed with the constructor REDUCER:

constructor REDUCER:
   \{addcontext: context * thm list -> context,
     apply:
     \{context: context,
       conv: term list -> term -> thm,
       relation: term * (term -> thm),
       solver: term list -> term -> thm, stack: term list\} ->
       Traverse.conv, initial: context, name: string option\} ->
     Traverse.reducer

The context type is an alias for the built-in SML type exn, that of exceptions. The exceptions here are used as a “universal type”, capable of storing data of any type. For example, if the desired data is a pair of an integer and a boolean, then the following declaration could be made:

   exception my_data of int * bool

It is not necessary to make this declaration visible with a wide scope. Indeed, only functions accessing and creating contexts of this form need to see it. For example:

  fun get_data c = (raise c) handle my_data (i,b) => (i,b)
  fun mk_ctxt (i,b) = my_data(i,b)

When creating a value of reducer type, the user must provide an initial context, and two functions. The first, addcontext, is called by the simplifier’s traversal mechanism to give every embedded decision procedure access to theorems representing new context information. For example, this function is called with theorems from the current assumptions in ASM_SIMP_TAC, and with the theorems from the theorem-list arguments to all of the various simplification functions. As a term is traversed, the congruence rules governing this traversal may also provide additional theorems; these will also be passed to the addcontext function. (Of course, it is entirely up to the addcontext function as to how these theorems will be handled; they may even be ignored entirely.)

When an embedded reducer is applied to a term, the provided apply function is called. As well as the term to be transformed, the apply function is also passed a record containing a side-condition solver, a more general call-back to the simplifier, the decision procedure’s current context, and the stack of side-conditions attempted so far. The stack and solver are the same as the additional arguments provided to user-supplied conversions. The conv argument is call-back to the simplifier, which given a term $t$ returns a theorem of the form $\vdash t = t'$ or fails. In contrast, the solver either returns the theorem $\vdash t$ or fails. The power of the reducer abstraction is having access to a context that can be built appropriately for each decision procedure.

Decision procedures are applied last when a term is encountered by the simplifier. More, they are applied after the simplifier has already recursed into any sub-terms and tried to do as much rewriting as possible. This means that although simplifier rewriting occurs in a top-down fashion, decision procedures will be applied bottom-up and only as a last resort.

As with user-conversions, decision procedures must raise an exception rather than return instances of reflexivity.

Special rewrite forms

Some of the simplifier’s features can be accessed in a relatively simple way by using SML functions to construct special theorem forms. These special theorems can then be passed in the simplification tactics’ theorem-list arguments.

Two of the simplifier’s advanced features, AC-normalisation and congruence rules can be accessed in this way. Rather than construct a custom simpset fragment including the required AC or congruence rules, the user can instead use the functions AC or Cong:

   AC : thm -> thm -> thm
   Cong : thm -> thm

For example, if the theorem value

   AC DISJ_ASSOC DISJ_COMM

appears amongst the theorems passed to a simplification tactic, then the simplifier will perform AC-normalisation of disjunctions. The Cong function provides a similar interface for the addition of new congruence rules.

Two other functions provide a crude mechanism for controlling the number of times an individual rewrite will be applied.

   Once : thm -> thm
   Ntimes : thm -> int -> thm

A theorem “wrapped” in the Once function will only be applied once when the simplifier is applied to a given term. A theorem wrapped in Ntimes will be applied as many times as given in the integer parameter.

Another pair of special forms allow the user to require that certain rewrites are applied. Both forms check the count of instances of rewrite-redexes appearing in the goal that results after simplification has happened. If the requirement is not satisfied, the relevant tactic fails. In this context, a rewrite redex is the LHS of a theorem being used as a rewrite, so that, for example, the redex of the theorem $\vdash x + 0 = x$ is $x + 0$. The Req0 form checks that the number of redexes of the corresponding rewrite is zero in the resulting goal. For unconditional rewrites, such a requirement is usually redundant, but this form can be useful when rewrites are conditional and the simplifier may have failed to discharge side-conditions. For example:

> val th = arithmeticTheory.ZERO_MOD;
val th = ⊢ ∀n. 0 < n ⇒ 0 MOD n = 0: thm
> simp[Req0 th] ([], ``0 MOD z``);
val it = ([([], “0”)], fn): goal list * validation

> simp[Req0 th] ([], ``0 MOD (z + 1)``)
   (* succeeds because arithmetic d.p. knows z + 1 is nonzero *);
val it = ([([], “0”)], fn): goal list * validation

The ReqD modifier requires that the redex count should have decreased. This is implicitly a check on the original goal as well: it must have a non-zero count of redexes itself.

Both Req0 and ReqD can be combined with Once and Ntimes.

Excluding rewrites

As also described above in Section 8.5.4.1, various built-in (named) components can be removed from invocations of the simplifier through the use of the Excl form. This function is of type string\ ->\ thm, so takes a string naming the rewrite or other component that is to be removed. For example, the standard stateful simpset includes the theorem stating that $x < x + y \Leftrightarrow 0 < y$, with name X_LT_X_PLUS:

> simp[] ([], “x < x + 2 * y”);
val it = ([([], “0 < y”)], fn): goal list * validation

> simp[Excl "X_LT_X_PLUS"] ([], “x < x + 2 * y”);
val it = ([([], “x < x + 2 * y”)], fn): goal list * validation

In addition to rewrites, conversions and decision procedures can also be temporarily excluded in this way:

> simp[Excl "BETA_CONV"] ([], “(λx. x + 10) (6 * z)”);
val it = ([([], “(λx. x + 10) (6 * z)”)], fn): goal list * validation
Excluding assumptions

It is possible to stop tactics such as simp from using assumptions (it otherwise tries to use all of a goal’s assumptions) with the NoAsms and IgnAsm forms. The NoAsms form prevents the use of all of a goal’s assumptions:

> simp[NoAsms] ([“x = 3”], “x < 10”);
val it = ([([“x = 3”], “x < 10”)], fn): goal list * validation

The IgnAsm form takes a quotation argument corresponding to a pattern (where free variables in the pattern that also occur in the goal are forced to take on their types in the goal). Every assumption that matches the pattern is excluded from further simplification. By default, the matching requires the pattern to match the entirety of the assumption statement. However, if the pattern concludes with the comment (* sa *) (with or without the spaces; “sa” stands for “sub-assumption”), the matching succeeds (and the assumption is excluded) if the pattern matches any sub-term of the assumption. Thus:

> simp[IgnAsm‘x = _’] ([“x = F”, “y = T”], “p ∧ x ∧ y”);
val it = ([([“x ⇔ F”, “y ⇔ T”], “p ∧ x”)], fn): goal list * validation

> simp[IgnAsm‘F’] ([“x = F”, “y = T”], “p ∧ x ∧ y”); (* nothing matches *)
val it = ([([“x ⇔ F”, “y ⇔ T”], “F”)], fn): goal list * validation

> simp[IgnAsm‘F(* sa *)’] ([“x = F”, “y = T”], “p ∧ x ∧ y”);
val it = ([([“x ⇔ F”, “y ⇔ T”], “p ∧ x”)], fn): goal list * validation

> simp[IgnAsm‘_ = _’] ([“x = F”, “y = T”, “p:bool”], “p ∧ x ∧ y”);
val it = ([([“x ⇔ F”, “y ⇔ T”, “p”], “x ∧ y”)], fn): goal list * validation
Including simpset fragments

The SF theorem form provides a way to augment a simplification with a simpset fragment. For example, one can rewrite with conjunctions as assumptions using the CONJ_ss fragment:

> g ‘x = 10 ∧ x < 16’;   ... output elided ...
> e (simp[SF CONJ_ss]);
OK..
1 subgoal:
val it =
   
   x = 10

SF also serves as the in-call escape hatch for fragments excluded by an enclosing Proof[exclude_frags\ =\ \ldots] attribute (see Section 8.5.4): writing simp[SF\ X_ss] inside a body where the fragment named "X" has been excluded re-enables it for that one simp call.

Simplifying at particular sub-terms

We have already seen (Section 8.5.6.1 above) that the simplifier’s congruence technology can be used to force the simplifier to ignore particular terms. The example in the section above discussed how a congruence rule might be used to ensure that only the guards of conditional expressions should be simplified.

In many proofs, it is common to want to rewrite only on one side or the other of a binary connective (often, this connective is an equality). For example, this occurs when rewriting with equations from complicated recursive definitions that are not just structural recursions. In such definitions, the left-hand side of the equation will have a function symbol attached to a sequence of variables, e.g.:

   |- f x y = ... f (g x y) z ...

Theorems of a similar shape are also returned as the “cases” theorems from inductive definitions.

Whatever their origin, such theorems are the classic example of something to which one would want to attach the Once qualifier. However, this may not be enough: one may wish to prove a result such as

   f (constructor x) y = ... f (h x y) z ...

(With relations, the goal may often feature an implication instead of an equality.) In this situation, one often wants to expand just the instance of f on the left, leaving the other occurrence alone. Using Once will expand only one of them, but without specifying which one is to be expanded.

The solution to this problem is to use special congruence rules, constructed as special forms that can be passed as theorems like Once. The functions

   SimpL : term -> thm
   SimpR : term -> thm

construct congruence rules to force rewriting to the left or right of particular terms. For example, if opn is a binary operator, SimpL\ ``(opn)``` returns Cong` applied to the theorem

   |- (x = x') ==> (opn x y = opn x' y)

Because the equality case is so common, the special values SimpLHS and SimpRHS are provided to force simplification on the left or right of an equality respectively. These are just defined to be applications of SimpL and SimpR to equality.

Note that these rules apply throughout a term, not just to the uppermost occurrence of an operator. Also, the topmost operator in the term need not be that of the congruence rule. This behaviour is an automatic consequence of the implementation in terms of congruence rules.

Limiting simplification

In addition to the Once and Ntimes theorem-forms just discussed, which limit the number of times a particular rewrite is applied, the simplifier can also be limited in the total number of rewrites it performs. The limit function (in simpLib and bossLib)

   limit : int -> simpset -> simpset

records a numeric limit in a simpset. When a limited simpset then works over a term, it will never apply more than the given number of rewrites to that term. When conditional rewrites are used, the rewriting done in the discharge of side-conditions counts against the limit, as long as the rewrite is ultimately applied. The application of user-provided congruence rules, user-provided conversions and decision procedures also all count against the limit.

When the simplifier yields control to a user-provided conversion or decision procedure it cannot guarantee that these functions will ever return (and they may also take arbitrarily long to work, often a worry with arithmetic decision procedures), but use of limit is otherwise a good method for ensuring that simplification terminates.

Rewriting with arbitrary pre-orders

In addition to simplifying with respect to equality, it is also possible to use the simplifier to “rewrite” with respect to a relation that is reflexive and transitive (a preorder). This can be a very powerful way of working with transition relations in operational semantics.

Imagine, for example, that one has set up a “deep embedding” of the $\lambda$-calculus. This will entail the definition of a new type (lamterm, say) within the logic, as well as definitions of appropriate functions (e.g., substitution) and relations over lamterm. One is likely to work with the reflexive and transitive closure of $\beta$-reduction ($\rightarrow^*_\beta$). This relation has congruence rules such as $$ \begin{array}{c@{\qquad\qquad}c} \frac{M_1 \;\rightarrow^*_\beta\;M_2}{M_1 \,N\;\rightarrow^*_\beta\;M_2\,N} & \frac{N_1 \;\rightarrow^*_\beta\;N_2}{M \,N_1\;\rightarrow^*_\beta\;M\,N_2}\\[3mm] \multicolumn{2}{c}{\frac{M_1\;\rightarrow^*_\beta\;M_2}{(\lambda v.M_1)\;\rightarrow^*_\beta\;(\lambda v.M_2)}} \end{array} $$ and one important rewrite $$ (\lambda v. M)\,N \;\rightarrow^*_\beta\; M[v := N] $$ Having to apply these rules manually in order to show that a given starting term can reduce to particular destination is usually very painful, involving many applications, not only of the theorems above, but also of the theorems describing reflexive and transitive closure (see Section 5.5.3).

Though the $\lambda$-calculus is non-deterministic, it is also confluent, so the following theorem holds: $$ \frac{\beta\textrm{-nf}\;N \qquad M_1 \;\rightarrow^*_\beta\;M_2} {M_1 \;\rightarrow^*_\beta\;N\;\;=\;\;M_2\;\rightarrow^*_\beta\; N} $$ This is the critical theorem that justifies the switch from rewriting with equality to rewriting with $\rightarrow^*_\beta$. It says that if one has a term $M_1\rightarrow^*_\beta N$, with $N$ a $\beta$-normal form, and if $M_1$ rewrites to $M_2$ under $\rightarrow^*_\beta$, then the original term is equal to $M_2\rightarrow^*_\beta N$. With luck, $M_2$ will actually be syntactically identical to $N$, and the reflexivity of $\rightarrow^*_\beta$ will prove the desired result. Theorems such as these, that justify the switch from one rewriting relation to another are known as weakening congruences.

When adjusted appropriately, the simplifier can be modified to exploit the five theorems above, and automatically prove results such as $$ u ((\lambda f\,x. f (f\,x)) v) \rightarrow^*_\beta u (\lambda x. v(v\,x)) $$ (on the assumption that the terms $u$ and $v$ are $\lambda$-calculus variables, making the result a $\beta$-normal form).

In addition, one will quite probably have various rewrite theorems that one will want to use in addition to those specified above. For example, if one has earlier proved a theorem such as $$ K\,x\,y \rightarrow^*_\beta x $$ then the simplifier can take this into account as well.

The function achieving all this is

   simpLib.add_relsimp  : {trans: thm, refl: thm, weakenings: thm list,
                           subsets: thm list, rewrs : thm list} ->
                          simpset -> simpset

The fields of the record that is the first argument are:

trans
The theorem stating that the relation is transitive, in the form $\forall x\;y\;z.\ R\,x\,y \land R\,y\,z \Rightarrow R\,x\,z$.
refl
The theorem stating that the relation is reflexive, in the form $\forall x.\ R\,x\,x$.
weakenings
A list of weakening congruences, of the general form $P_1 \Rightarrow P_2 \Rightarrow \cdots (t_1 = t_2)$, where at least one of the $P_i$ will presumably mention the new relation $R$ applied to a variable that appears in $t_1$. Other antecedents may be side-conditions such as the requirement in the example above that the term $N$ be in $\beta$-normal form.
subsets
Theorems of the form $R'\,x\,y \Rightarrow R\,x\,y$. These are used to augment the resulting simpset’s “filter” so that theorems in the context mentioning $R'$ will derive useful rewrites involving $R$. In the example of $\beta$-reduction, one might also have a relation $\rightarrow_{wh}^*$ for weak-head reduction. Any weak-head reduction is also a $\beta$-reduction, so it can be useful to have the simplifier automatically “promote” facts about weak-head reduction to facts about $\beta$-reduction, and to then use them as rewrites.
rewrs
Possibly conditional rewrites, presumably mostly of the form $P \Rightarrow R\,t_1\,t_2$. Rewrites over equality can also be included here, allowing useful additional facts to be included. For example, when working with the $\lambda$-calculus, one might include both the rewrite for $K$ above, as well as the definition of substitution.

The application of this function to a simpset ss will produce an augmented ss that has all of ss’s existing behaviours, as well as the ability to rewrite with the given relation.

Tracing the Simplifier

There is a trace variable associated with the simplifier that can be used to obtain a log of its activities printed to the screen as simplification proceeds. (The tracing system is described generally in Section 10.2 below.) With the name "simplifier", this trace can be set to have integer values between 0 and 7, inclusive. The default value of 0 means that no logging will be printed. Larger values result in more output.

Tracing can be useful in trying to determine why a simplification theorem is not being applied, perhaps because of a failure to simplify side-conditions. At high values of the trace, output can be particularly voluminous.

Pattern Matches Library — patternMatchesLib

Pattern Matches Library — patternMatchesLib

HOL supports two different types of case expressions: decision tree based and \pmatch{} case expressions. These are presented in Section 7.5. In subsection 7.5.2, the basic usage of \pmatch{} case expressions is discussed. Some concepts presented there briefly are discussed here in detail. Moreover, advanced features are discussed here.

Simplification

The most important tool to deal with \pmatch{} case expressions is the conversion PMATCH_SIMP_CONV or the corresponding PMATCH_SIMP_ss, which is part of bossLib.std_ss. It combines the following methods of simplifying and (partially) evaluating \pmatch{} case expressions. A subset of these methods that skips normalisations and potentially expensive searches for redundant and subsumed rows is available as PMATCH_FAST_SIMP_CONV.

Normalisation

Many simplifications rely on the variables of a pattern being named consistently and no extra, unused pattern variables being present. The conversion PMATCH_CLEANUP_PVARS_CONV removes unused pattern variables and ensures that the names of variables used by the pattern, the guard and the right-hand-side of a row coincide.

> PMATCH_CLEANUP_PVARS_CONV ``PMATCH (x:('a # 'b) option) [
    PMATCH_ROW (\x:'a. NONE) (\x. T) (\x. 5);
    PMATCH_ROW (\ (x,(y:'c)). SOME (x,z)) (\ (x,y). T) (\ (x,y). 8);
    PMATCH_ROW (\ (x,z). SOME (x,z)) (\_. T) (\ (a,y). 8)]``
val it =
   ⊢ PMATCH x
       [PMATCH_ROW (λx. NONE) (λx. T) (λx. 5);
        PMATCH_ROW (λ(x,y). SOME (x,z)) (λ(x,y). T) (λ(x,y). 8);
        PMATCH_ROW (λ(x,z). SOME (x,z)) (λ_0. T) (λ(a,y). 8)] =
     pmatch x of NONE => 5 | x .| SOME (x,z) => 8 | SOME (x,z) => 8: thm

Similarly, many \pmatch{} tools rely on each pattern of a case expression having the same number of columns. This normal form is enforced by PMATCH_EXPAND_COLS_CONV.

> PMATCH_EXPAND_COLS_CONV ``pmatch (x,y,z) of
   (0,y,T) => y
  | xyz when ~ SND (SND xyz) => 2
  | (x,yz) => x``
val it =
   ⊢ (pmatch (x,y,z) of
       (0,y,T) => y | xyz when ¬SND (SND xyz) => 2 | (x,yz) => x) =
     pmatch (x,y,z) of
       (0,y,T) => y
     | (xyz_0,xyz_1,xyz_2) when ¬SND (SND (xyz_0,xyz_1,xyz_2)) => 2
     | (x,yz_0,yz_1) => x: thm

Finally, the conversion PMATCH_INTRO_WILDCARDS_CONV renames unused pattern variables such that they start with an underscore. As a result, they are printed as a wildcard pattern, making case expressions more readable. It also renames used variables that start with an underscore. This is rarely needed, though.

> PMATCH_INTRO_WILDCARDS_CONV ``pmatch (x,y,z) of
   (_x, y, z) => _x + y
  | (x, y, z) when z => x``
val it =
   ⊢ (pmatch (x,y,z) of (_,y,z) => _ + y | (x,y,z) when z => x) =
     pmatch (x,y,z) of (v0,y,_) => v0 + y | (x,_,z) when z => x: thm

A combination of these conversions for normalising \pmatch{} case expressions is available as PMATCH_NORMALISE_CONV.

(Partial) evaluation

The function PMATCH_CLEANUP_CONV checks each row of a \pmatch{} case expression and determines whether it matches the tested expression. There are three possible outcomes of such a check: a proof that the row matches, a proof that the row does not match or that it could not be decided whether the row matches. Rows that are proved to not match are removed. Similarly, all rows after the first matching row are redundant and are removed. If the first remaining row is known to match, the whole case expression is evaluated.

The proof of whether a row matches is attempted using some default proof methods. In particular information about datatype constructors is automatically used from TypeBase and constrFamiliesLib (see Section 8.9.6.2). If used via PMATCH_SIMP_ss, a callback to the simplifier is used. The conversion PMATCH_CLEANUP_CONV_GEN is a generalised version of the partial evaluation conversion that allows manually providing additional simpset fragments to the used proof method.

In the following example, the first row is removed, because it does not match. The second line is kept, since depending on the value of y it might or might not match. Since the third line matches in any case, the fourth one is deleted.

> PMATCH_CLEANUP_CONV ``pmatch (SOME (x:num),y) of
    (NONE, y) => 1
  | (x, 0) => 2
  | (SOME x, y) => 3
  | (x, y) => 4``
val it =
   ⊢ (pmatch (SOME x,y) of
       (NONE,y) => 1 | (x,0) => 2 | (SOME x,y) => 3 | (x,y) => 4) =
     pmatch (SOME x,y) of (x,0) => 2 | (SOME x,y) => 3: thm

If the first row remaining matches, the case expression is evaluated:

> PMATCH_CLEANUP_CONV ``pmatch (SOME x, y) of
    (NONE, y) => 1
  | (SOME x, y) => x+y
  | (x, y) => 4``
val it =
   ⊢ (pmatch (SOME x,y) of (NONE,y) => 1 | (SOME x,y) => x + y | (x,y) => 4) =
     x + y: thm

Similarly, if no row matches, the whole case expression is evaluated.

> PMATCH_CLEANUP_CONV ``pmatch (SOME (x:num), y:num) of (NONE, y) => 1``
val it = ⊢ (pmatch (SOME x,y) of (NONE,y) => 1) = PMATCH_INCOMPLETE: thm

Simplifying columns

Before, we saw how rows can be removed. PMATCH_SIMP_COLS_CONV allows removing a column of a \pmatch{} case expression. If for all rows a certain column matches the input value for this column, the column can be removed. This situation usually arises after removing certain rows from a case expression via partial evaluation.

> PMATCH_SIMP_COLS_CONV ``pmatch (SOME x,y) of
  | (SOME x, 1) => x+y
  | (x, y) => 4``
val it =
   ⊢ (pmatch (SOME x,y) of (SOME x,1) => x + y | (x,y) => 4) =
     pmatch y of 1 => x + y | y => 4: thm

Similarly, a column is partially evaluated if all rows contain either a variable, a wildcard or a term of the same constructor in this column.

> PMATCH_SIMP_COLS_CONV ``pmatch (SOME x,y) of
  | (SOME x, 1) => SOME (x+y)
  | (SOME 2, 2) => NONE
  | (x, y) => x``
val it =
   ⊢ (pmatch (SOME x,y) of
       (SOME x,1) => SOME (x + y) | (SOME 2,2) => NONE | (x,y) => x) =
     pmatch (x,y) of
       (x,1) => SOME (x + y) | (2,2) => NONE | (x_0,y) => SOME x_0: thm

Removing redundant rows

The simplifications above easily lead to case expressions that contain multiple similar rows. The conversion PMATCH_REMOVE_FAST_REDUNDANT_CONV is intended to cleanup such rows. A row is called redundant if each value that matches it also matches an earlier row. Redundant rows will never matter and can therefore safely be removed. Thus the conversion PMATCH_REMOVE_FAST_REDUNDANT_CONV checks whether a pattern of a row is an instance of a pattern of an earlier row. This simple, fast heuristic is sufficient to detect most instances of redundant rows occurring during simplification. In the following example, the rows with right-hand-side 2, 4 and 5 are redundant. However, this simple heuristic cannot detect that row 5 is redundant. A more advanced method for removing redundant rows, which is slower but for example able to detect that row 5 is redundant, is discussed in Section 8.9.7.

> PMATCH_REMOVE_FAST_REDUNDANT_CONV ``pmatch xy of
  | (SOME x, y) => 1 | (SOME 2, 3) => 2
  | (NONE, y) => 3 | (NONE, y) => 4
  | (x, 5) => 5``
val it =
   ⊢ (pmatch xy of
       (SOME x,y) => 1
     | (SOME 2,3) => 2
     | (NONE,y) => 3
     | (NONE,y) => 4
     | (x,5) => 5) =
     pmatch xy of (SOME x,y) => 1 | (NONE,y) => 3 | (x,5) => 5: thm

Removing subsumed rows

Redundant rows are rows that are not needed, because they are shadowed by an earlier row. Similarly, subsumed rows are rows that can be dropped, because in case they match a later row matches as well and evaluates to the same value. It is trickier to check for subsumed rows, because one needs to check that no row between the subsuming row and the possibly subsumed row matches, and because the right hand sides of the rows need to be considered as well. The function PMATCH_REMOVE_FAST_SUBSUMED_CONV removes subsumed rows that can be detected quickly.

If no row matches, a \pmatch{} case expression evaluates to ARB. Therefore, a row with right-hand-side of ARB is considered to be subsumed if no further row matches. This is not always what users expect or want. For example, the user might not want to see an exhaustive pattern match turn into a non-exhaustive one. Thus PMATCH_REMOVE_FAST_SUBSUMED_CONV takes an additional boolean argument ra, which allows one to configure whether such rows are removed.

> PMATCH_REMOVE_FAST_SUBSUMED_CONV true ``pmatch xy of
  | (SOME 2, _) => 2 | (NONE, 3) => 1
  | (SOME x, _) => x | (NONE, y) => y
  | (x, 5) => ARB``
val it =
   ⊢ (pmatch xy of
       (SOME 2,_) => 2
     | (NONE,3) => 1
     | (SOME x,_) => x
     | (NONE,y) => y
     | (x,5) => ARB) =
     pmatch xy of (NONE,3) => 1 | (SOME x,_) => x | (NONE,y) => y: thm
> PMATCH_REMOVE_FAST_SUBSUMED_CONV false ``pmatch xy of
  | (SOME 2, _) => 2 | (NONE, 3) => 1
  | (SOME x, _) => x | (NONE, y) => y
  | (x, 5) => ARB``
val it =
   ⊢ (pmatch xy of
       (SOME 2,_) => 2
     | (NONE,3) => 1
     | (SOME x,_) => x
     | (NONE,y) => y
     | (x,5) => ARB) =
     pmatch xy of
       (NONE,3) => 1 | (SOME x,_) => x | (NONE,y) => y | (x,5) => ARB: thm

The PMATCH_SIMP_CONV conversion keeps such rows.

> PMATCH_SIMP_CONV ``pmatch xy of
  | (SOME 2, _) => 2 | (NONE, 3) => 1
  | (SOME x, _) => x | (NONE, y) => y
  | (x, 5) => ARB``
val it =
   ⊢ (pmatch xy of
       (SOME 2,_) => 2
     | (NONE,3) => 1
     | (SOME x,_) => x
     | (NONE,y) => y
     | (x,5) => ARB) =
     pmatch xy of
       (NONE,3) => 1 | (SOME x,_) => x | (NONE,y) => y | (_,5) => ARB: thm

Support for computeLib

The conversion PMATCH_CLEANUP_CONV (see Section 8.9.1.2) is added to the internal database of computeLib. This allows the efficient evaluation of ground terms that contain \pmatch{} case expressions.

> EVAL ``pmatch (SOME 3, SOME 4) of
  | (SOME x, SOME y) => SOME (x + y)
  | (_, _) => NONE``
val it =
   ⊢ (pmatch (SOME 3,SOME 4) of
       (SOME x,SOME y) => SOME (x + y) | (_,_) => NONE) =
     SOME 7: thm
> EVAL ``pmatch (NONE, SOME 4) of
  | (SOME x, SOME y) => SOME (x + y)
  | (_, _) => NONE``
val it =
   ⊢ (pmatch (NONE,SOME 4) of (SOME x,SOME y) => SOME (x + y) | (_,_) => NONE) =
     NONE: thm

Removing extra features

\pmatch{} case expressions support features that are not usually supported by programming languages. One can use the same pattern variable multiple times in a pattern and use variables not bound by a pattern. Moreover, there is support for guards.

Sometimes, it is desirable to remove such features from a \pmatch{} case expression. A typical example is that they need to be removed before code-extraction.

Normalising pattern variables

The function PMATCH_REMOVE_DOUBLE_BIND_CONV and the corresponding simpset fragment PMATCH_REMOVE_DOUBLE_BIND_ss remove variables bound multiple times by a pattern as well as variables not bound by the pattern. This is easily achievable by introducing extra variables into the pattern and constraining their value by adding extra conditions to the guard.

> PMATCH_REMOVE_DOUBLE_BIND_CONV ``pmatch xy of
  | (x, x) when x > 0 => x + x
  | x.| (x, y) => x
  | (x, _) => SUC x``
val it =
   ⊢ (pmatch xy of
       (x,x) when x > 0 => x + x | x .| (x,y) => x | (x,_) => SUC x) =
     pmatch xy of
       (x,x') when x' = x ∧ x > 0 => x + x
     | (x,y') when y' = y => x
     | (x,_) => SUC x: thm

Removing guards

Guards can be removed by introducing an if-then-else expression on the right-hand-side. The else-part of this if-then-else expression needs to continue the case-split with the rows occurring after the row whose guard is removed. Usually this case expression can be simplified significantly, since we know that the input matches the pattern of the row, whose guard is removed. Therefore, the conversion PMATCH_REMOVE_GUARDS_CONV as well as the corresponding PMATCH_REMOVE_GUARDS_ss internally call PMATCH_SIMP_CONV.

> PMATCH_REMOVE_GUARDS_CONV ``pmatch (x, y) of
  | (x, 2) when EVEN x => x + x
  | (SUC x, y) when ODD x => y + x + SUC x
  | (SUC x, 1) => x
  | (x, _) => x+3``
val it =
   ⊢ (pmatch (x,y) of
       (x,2) when EVEN x => x + x
     | (SUC x,y) when ODD x => y + x + SUC x
     | (SUC x,1) => x
     | (x,_) => x + 3) =
     pmatch (x,y) of
       (x,2) =>
            if EVEN x then x + x
            else pmatch x of SUC x when ODD x => 2 + x + SUC x | x => x + 3
     | (SUC x,y) =>
            if ODD x then y + x + SUC x
            else pmatch y of 1 => x | _ => SUC x + 3
     | (x,_) => x + 3: thm
> PMATCH_REMOVE_GUARDS_CONV ``pmatch (x, y) of
  | (x, 0) when EVEN x => (SOME x, T)
  | (x, 0) => (SOME x, F)
  | (0, _) => (NONE, T)
  | (_, _) => (NONE, F)``
val it =
   ⊢ (pmatch (x,y) of
       (x,0) when EVEN x => (SOME x,T)
     | (x,0) => (SOME x,F)
     | (0,_) => (NONE,T)
     | (_,_) => (NONE,F)) =
     pmatch (x,y) of
       (x,0) => if EVEN x then (SOME x,T) else (SOME x,F)
     | (0,_) => (NONE,T)
     | (_,_) => (NONE,F): thm
> SIMP_CONV (std_ss ++ PMATCH_REMOVE_GUARDS_ss) [] ``pmatch x of
  | _ when x < 5 => 0
  | _ when x < 10 => 1
  | _ => 2``
val it =
   ⊢ (pmatch x of _ when x < 5 => 0 | _ when x < 10 => 1 | _ => 2) =
     if x < 5 then 0 else if x < 10 then 1 else 2: thm

Lifting case expressions

HOL provides powerful tools for rewriting. Probably the most commonly used way of using case expressions in HOL is at top-level for defining recursive functions. Special support in Define turns multiple top-level equations into a decision tree case expressions, uses this case expression for defining a function and then derives top-level equations similar to the input ones. Since compilation to decision trees is used, the issues discussed in Section 7.5 are present. It is sometimes hard to predict, which equations will be generated. There might be a blow-up in the number of equations. Moreover, equations cannot overlap and are therefore often unnecessarily complicated.

As an example consider the following definition of a zipping functions for lists.

> val MYZIP_def = Define `
  (MYZIP [] _ = []) /\
  (MYZIP _ [] = []) /\
  (MYZIP (x::xs) (y::ys) = (x,y) :: (MYZIP xs ys))`   ... output elided ...

val MYZIP_def =
   ⊢ (∀v0. MYZIP [] v0 = []) ∧ (∀v4 v3. MYZIP (v3::v4) [] = []) ∧
     ∀ys y xs x. MYZIP (x::xs) (y::ys) = (x,y)::MYZIP xs ys: thm
> val MYZIP2_def = Pmatch.with_classic_heuristic Define `
  (MYZIP2 [] _ = []) /\
  (MYZIP2 _ [] = []) /\
  (MYZIP2 (x::xs) (y::ys) = (x,y) :: (MYZIP2 xs ys))`   ... output elided ...

val MYZIP2_def =
   ⊢ MYZIP2 [] [] = [] ∧ (∀v8 v7. MYZIP2 [] (v7::v8) = []) ∧
     (∀v4 v3. MYZIP2 (v3::v4) [] = []) ∧
     ∀ys y xs x. MYZIP2 (x::xs) (y::ys) = (x,y)::MYZIP2 xs ys: thm

We can use \pmatch{} case expressions to fight these issues. There is, however, no special support for \pmatch{} case expressions built into Define. Instead, one needs to define a function with a \pmatch{} case expression on the right-hand-side. Using the rule PMATCH_TO_TOP_RULE then produces the desired (conditional) equations.

> val MYZIP3_def = Define `
  MYZIP3 xl yl = (pmatch (xl, yl) of
  | ([], _) => []
  | (_, []) => []
  | (x::xs, y::ys) => (x,y) :: (MYZIP3 xs ys))`   ... output elided ...

> val MYZIP3_EQS = PMATCH_TO_TOP_RULE MYZIP3_def
val MYZIP3_EQS =
   ⊢ (∀yl. MYZIP3 [] yl = []) ∧ (∀xl. MYZIP3 xl [] = []) ∧
     ∀x xs y ys. MYZIP3 (x::xs) (y::ys) = (x,y)::MYZIP3 xs ys: thm

Similarly, the resulting induction theorems are more predictable and contain fewer cases. However, the structure tends not to be as nice.

val MYZIP_ind =
   ⊢ ∀P. (∀v0. P [] v0) ∧ (∀v3 v4. P (v3::v4) []) ∧
         (∀x xs y ys. P xs ys ⇒ P (x::xs) (y::ys)) ⇒
         ∀v v1. P v v1: thm
val MYZIP2_ind =
   ⊢ ∀P. P [] [] ∧ (∀v7 v8. P [] (v7::v8)) ∧ (∀v3 v4. P (v3::v4) []) ∧
         (∀x xs y ys. P xs ys ⇒ P (x::xs) (y::ys)) ⇒
         ∀v v1. P v v1: thm
val MYZIP3_ind =
   ⊢ ∀P. (∀xl yl.
            (∀x xs y ys. (xl,yl) = (x::xs,y::ys) ∧ T ⇒ P xs ys) ⇒ P xl yl) ⇒
         ∀v v1. P v v1: thm

For the zipping examples the resulting equations are particularly nice. In general, conditional equations need to be generated. The preconditions state that no previous row matched or that the result of such a matching row coincides with the result of the current row.

> val MYZIP4_def = Define `
  MYZIP4 xl yl = (pmatch (xl, yl) of
  | ([], []) => (NONE, [])
  | ([], _) => (SOME T, [])
  | (_, []) => (SOME F, [])
  | (x::xs, y::ys) => (case (MYZIP4 xs ys) of
     | (r, l) => (r, (x,y)::l)))`   ... output elided ...

> val MYZIP4_EQS = PMATCH_TO_TOP_RULE MYZIP4_def
val MYZIP4_EQS =
   ⊢ MYZIP4 [] [] = (NONE,[]) ∧ (∀yl. yl ≠ [] ⇒ MYZIP4 [] yl = (SOME T,[])) ∧
     (∀xl. xl ≠ [] ⇒ MYZIP4 xl [] = (SOME F,[])) ∧
     ∀x xs y ys.
       MYZIP4 (x::xs) (y::ys) = case MYZIP4 xs ys of (r,l) => (r,(x,y)::l):
   thm

The lifting functionality is also available via PMATCH_LIFT_BOOL_ss and PMATCH_LIFT_BOOL_CONV, which lift a \pmatch{} case expression to the next highest boolean level and expands it there. Since trying to prove exhaustiveness (see Section 8.9.9) might be slow, there is flag for turning it on and off explicitly. Moreover, notice that PMATCH_LIFT_BOOL_CONV always tries to lift to the top-level. Therefore, it should usually be combined with something like DEPTH_CONV.

> DEPTH_CONV (PMATCH_LIFT_BOOL_CONV true) ``
  P /\ (f (pmatch x of [] => 0 | x::xs => x) = 5) /\ Q``
val it =
   ⊢ P ∧ f (pmatch x of [] => 0 | x::xs => x) = 5 ∧ Q ⇔
     P ∧ ((x = [] ⇒ f 0 = 5) ∧ ∀x' xs. x = x'::xs ⇒ f x' = 5) ∧ Q: thm
> DEPTH_CONV (PMATCH_LIFT_BOOL_CONV false) ``
  P /\ (f (pmatch x of [] => 0 | x::xs => x) = 5) /\ Q``
val it =
   ⊢ P ∧ f (pmatch x of [] => 0 | x::xs => x) = 5 ∧ Q ⇔
     P ∧
     ((x = [] ⇒ f 0 = 5) ∧ (∀x' xs. x = x'::xs ⇒ f x' = 5) ∧
      (¬PMATCH_IS_EXHAUSTIVE x
         [PMATCH_ROW (λ_. []) (λ_. T) (λ_. 0);
          PMATCH_ROW (λ(x,xs). x::xs) (λ(x,xs). T) (λ(x,xs). x)] ⇒
       f ARB = 5)) ∧ Q: thm

Translating \pmatch{} and decision tree case expressions

As discussed in Section 7.5, there are benefits to both \pmatch{} and decision tree based case expressions. Therefore, there are tools for translating between both representations.

The function pmatch2case uses the pattern compilation algorithm implemented in HOL's parser to generate decision tree case expressions. This is done outside the logic without any formal justification. However, a brute force method that repeatedly performs case splits and evaluates is sufficient for proving equivalence. This leads to PMATCH_ELIM_CONV.

Only \pmatch{} case expressions that fall into the subset supported by decision tree ones can be translated. This means that no guards can be used and that all patterns need to be constructor patterns.

> PMATCH_ELIM_CONV
  ``pmatch (xy:(num option # num list)) of (NONE, x::xs) => 0``
<<HOL message: mk_functional: 
  pattern completion has added 2 clauses to the original specification.>>
val it =
   ⊢ (pmatch xy of (NONE,x::xs) => 0) =
     case xy of (v,[]) => ARB | (NONE,x::xs) => 0 | (SOME v5,x::xs) => ARB:
   thm

An approach similar to the one implemented in HOL's pretty printer allows the translation of decision tree case expressions to equivalent \pmatch{} expressions. The underlying function is case2pmatch do_opt, where the do_opt flag determines whether certain non-trivial optimisations are attempted. The corresponding conversions are named PMATCH_INTRO_CONV and PMATCH_INTRO_CONV_NO_OPTIMISE.

> PMATCH_INTRO_CONV
  ``case (xy:(num option # num list)) of (NONE, x::xs) => 0``
<<HOL message: mk_functional: 
  pattern completion has added 2 clauses to the original specification.>>
val it =
   ⊢ (case xy of (v,[]) => ARB | (NONE,x::xs) => 0 | (SOME v5,x::xs) => ARB) =
     pmatch xy of (NONE,_::_) => 0: thm
> PMATCH_INTRO_CONV_NO_OPTIMISE
  ``case (xy:(num option # num list)) of (NONE, x::xs) => 0``
<<HOL message: mk_functional: 
  pattern completion has added 2 clauses to the original specification.>>
val it =
   ⊢ (case xy of (v,[]) => ARB | (NONE,x::xs) => 0 | (SOME v5,x::xs) => ARB) =
     pmatch xy of (v,[]) => ARB | (NONE,x::xs) => 0 | (SOME v5,x::xs) => ARB:
   thm

Pattern Compilation

The pmatch2case function allows \pmatch{} case expressions to be compiled into decision tree case expressions. It is fast and the result is usually pretty good. However, it relies on the pattern compilation implementation of HOL's parser. This has several drawbacks. The most significant one is that it is an all-or-nothing approach. Either the compilation succeeds and we get an equivalent decision tree case expression (without proof) or it fails and one has nothing. It is not easily possible to get partial results or use the information obtained during pattern compilation to prove exhaustiveness or find a set of missing patterns. With simplification of \pmatch{} case expressions (see Section 8.9.1) in place, it is straightforward to implement pattern compilation. One performs a case-split on one variable occurring in the input of the case expression, simplifies and iterates. This is implemented as PMATCH_CASE_SPLIT_CONV. Note that PMATCH_CASE_SPLIT_CONV has no support for guards or pattern variables bound multiple times. These features need to be removed (see Section 8.9.3) before calling it.

> PMATCH_CASE_SPLIT_CONV
  ``pmatch l of (SOME x, SOME y) => SOME (x+y) | (_, _) => NONE``
val it =
   ⊢ (pmatch l of (SOME x,SOME y) => SOME (x + y) | (_,_) => NONE) =
     case l of
       (NONE,v') => NONE
     | (SOME x',NONE) => NONE
     | (SOME x',SOME x'') => SOME (x' + x''): thm

The trick is to choose which case split to apply next. This decision is taken by two mechanisms: a column heuristic picks the column to perform a case split on and the available case-splits are maintained by the constructor family library.

Column Heuristic

The most important decision during pattern compilation is which column, i.e., which input variable to perform a case split on next. Different decisions lead to different decision trees, which can differ significantly in size and time needed to evaluate. It is not trivial to find a good column to split on. Currently, mainly heuristics presented by Maranget (Maranget 2008) are implemented. In HOL a column heuristic is a ML function of type column_heuristic. Given a list of columns such a heuristic returns the number of the column to perform a split on. There are very simple heuristics like always picking the first or last column, but also sophisticated ones like qba (see (Maranget 2008)). Users can easily implement additional heuristics should the need arise. Figure 8.9.6.1 shows the effects of using different heuristics.

Figure 8.9.6.1. Effect of different column heuristics.

val t = ...

> PMATCH_CASE_SPLIT_CONV_HEU colHeu_first_col t
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
val it =
   ⊢ (pmatch (x,y,z) of
       (_,F,T) => 1 | (F,T,_) => 2 | (_,_,F) => 3 | (_,_,T) => 4) =
     case (x,y,z) of
       (T,T,T) => 4
     | (T,T,F) => 3
     | (T,F,T) => 1
     | (T,F,F) => 3
     | (F,T,v'³') => 2
     | (F,F,T) => 1
     | (F,F,F) => 3: thm

> PMATCH_CASE_SPLIT_CONV_HEU colHeu_last_col t
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
val it =
   ⊢ (pmatch (x,y,z) of
       (_,F,T) => 1 | (F,T,_) => 2 | (_,_,F) => 3 | (_,_,T) => 4) =
     case (x,y,z) of
       (T,T,T) => 4
     | (F,T,T) => 2
     | (v,F,T) => 1
     | (T,T,F) => 3
     | (F,T,F) => 2
     | (v,F,F) => 3: thm

> PMATCH_CASE_SPLIT_CONV_HEU colHeu_default t
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
metis: r[+0+5]+0+0+1+1+2+0+1+0+0#
val it =
   ⊢ (pmatch (x,y,z) of
       (_,F,T) => 1 | (F,T,_) => 2 | (_,_,F) => 3 | (_,_,T) => 4) =
     case (x,y,z) of
       (T,T,T) => 4
     | (T,T,F) => 3
     | (F,T,v'³') => 2
     | (v,F,T) => 1
     | (v,F,F) => 3: thm

Constructor Family Library

Once a column has been chosen, a case split needs to be performed. This requires getting information about an appropriate case split function. Moreover, even for selecting the column some heuristics need information. It might for example be desirable to know to how many cases splitting on a column would lead.

Essentially, one needs to lookup the constructors of a datatype together with its case-constant. Moreover, theorems about injectivity and pairwise distinctiveness of the constructors as well as some theorems about the case-constant are needed. All this information can be found in TypeBase (see Section 7.2). The pattern compilation algorithm in the parser uses TypeBase. However, patternMatchesLib has two demands not met by TypeBase. For each type, it should be possible to store multiple sets of constructors. Moreover, sometimes the case split should not be stored statically but — given a column — be computed dynamically. These demands lead to the implementation of constrFamiliesLib.

The constructor family library contrFamiliesLib is a library for collecting information about constructors and case-splits. At its core is the concept of a constructor family. A constructor family is a list of functions together with a case-split functions. The functions should all be injective and pairwise distinct. The case-split function should provide a case-analysis that corresponds with the functions. Constructor families can be exhaustive or inexhaustive. For inexhaustive ones, the case-split function has to provide an extra otherwise-case.

The constructors of datatypes together with the case constant form constructor families. For example, the constructors [] and CONS with list_CASE form an exhaustive constructor family for lists. The information for the constructor families corresponding to the datatype constructors is automatically extracted from TypeBase and available via constrFamiliesLib. However, there might be other interesting constructor families. For example, [] and SNOC together with an appropriate case split function form another exhaustive constructor family for lists.

First, we need to define a case-split function for [] and SNOC.

> val list_REVCASE_def = Define `
    list_REVCASE l c_nil c_snoc =
      (if l = [] then c_nil else (c_snoc (LAST l) (BUTLAST l)))`   ... output elided ...

Next, we define an exhaustive list of constructors. This is the list of functions combined with names for the arguments of each constructor.

> open constrFamiliesLib   ... output elided ...
> val cl = make_constructorList true [
     (``[]:'a list``, []),
     (``SNOC: 'a -> 'a list -> 'a list``,  ["x", "xs"])]
val cl =
   {cl_constructors = [CONSTR (“[]”, []), CONSTR (“SNOC”, ["x", "xs"])],
    cl_is_exhaustive = true, cl_type = “:α list”}: constructorList

The function mk_constructorFamily is then used to create a constructor family. This requires proving the discussed properties. In order to develop the necessary tactic, set_constructorFamily can be used.

> set_constructorFamily (cl, ``list_REVCASE``)
val it =
   Proof manager status: 2 proofs.
   2. Incomplete goalstack:
        Initial goal:
        x = 10 ∧ x < 16
        
        Current goal:
        x = 10
   
   1. Incomplete goalstack:
        Initial goal:
        (∀x xs x' xs'. SNOC x xs = SNOC x' xs' ⇔ x = x' ∧ xs = xs') ∧
        ((∀x xs. [] ≠ SNOC x xs) ∧ ∀x xs. SNOC x xs ≠ []) ∧
        (∀ff x. ff x = list_REVCASE x (ff []) (λx' xs. ff (SNOC x' xs))) ∧
        (∀x' f1 f2 x f1' f2'.
           x' = x ⇒
           (x = [] ⇒ f1 = f1') ⇒
           (∀x' xs. x = SNOC x' xs ⇒ f2 x' xs = f2' x' xs) ⇒
           list_REVCASE x' f1 f2 = list_REVCASE x f1' f2') ∧
        ∀x. x = [] ∨ ∃x' xs. x = SNOC x' xs

> val cf = mk_constructorFamily (cl, ``list_REVCASE``, ... some tactic ...)
val cf =
   {case_cong_thm =
    ⊢ ∀x' f1 f2 x f1' f2'.
        x' = x ⇒
        (x = [] ⇒ f1 = f1') ⇒
        (∀x' xs. x = SNOC x' xs ⇒ f2 x' xs = f2' x' xs) ⇒
        list_REVCASE x' f1 f2 = list_REVCASE x f1' f2', case_const =
    “list_REVCASE”, case_split_thm =
    ⊢ ∀ff x. ff x = list_REVCASE x (ff []) (λx' xs. ff (SNOC x' xs)),
    constructors =
    {cl_constructors = [CONSTR (“[]”, []), CONSTR (“SNOC”, ["x", "xs"])],
     cl_is_exhaustive = true, cl_type = “:α list”}, distinct_thm =
    SOME ⊢ (∀x xs. [] ≠ SNOC x xs) ∧ ∀x xs. SNOC x xs ≠ [], nchotomy_thm =
    SOME ⊢ ∀x. x = [] ∨ ∃x' xs. x = SNOC x' xs, one_one_thm =
    SOME ⊢ ∀x xs x' xs'. SNOC x xs = SNOC x' xs' ⇔ x = x' ∧ xs = xs'}:
   constructorFamily

Finally, we can register this newly defined constructor family.

> val _ = pmatch_compile_db_register_constrFam cf

Now this new family is available for pattern compilation. Notice, that the old constructors for lists are still present.

> PMATCH_CASE_SPLIT_CONV ``pmatch ll of
    (SNOC x xs, []) => x
  | ([], x::xs) => x
  | (_, _) => 0``
metis: r[+0+5]+0+0+0+1+0+2#
val it =
   ⊢ (pmatch ll of (SNOC x xs,[]) => x | ([],x::xs) => x | (_,_) => 0) =
     case ll of
       (v,v') =>
         list_REVCASE v (case v' of [] => 0 | h::t => h)
           (λx' xs. case v' of [] => x' | h'::t' => 0): thm

Inexhaustive constructor families are often handy as well. Consider the example of red-black-trees defined as follows:

> val _ = Datatype `
  tree = Empty
       | Red tree 'a tree
       | Black tree 'a tree`;   ... output elided ...

A lot of functions (e.g., balancing) treat black nodes and leaves the same. However, when compiling corresponding case expressions to decision trees, 3 cases instead of the required 2 are produced. Defining an inexhaustive constructor family for just the RED constructor solves this issue (see Figure 8.9.6.2).

Figure 8.9.6.2. Example inexhaustive constructor family.

val tree_red_CASE_def = Define tree_red_CASE tr f_red f_else = tree_CASE tr (f_else Empty) f_red (\t1 n t2. f_else (Black t1 n t2)) ... output elided ... val cl = make_constructorList false [(Red, ["t1", "n", "t2"])] ... output elided ...

> val cf = mk_constructorFamily (cl, ``tree_red_CASE``, ... some tactic ...)
> val _ = pmatch_compile_db_register_constrFam cf   ... output elided ...

> PMATCH_CASE_SPLIT_CONV ``pmatch (t:'a tree) of
    | Red _ _ _ => T
    | _ => F``
val it =
   ⊢ (pmatch t of Red _ _ _ => T | _ => F) ⇔
     tree_red_CASE t (λt1 n t2. T) (λx. F): thm

> PMATCH_CASE_SPLIT_CONV ``pmatch (t:'a tree) of
    | Black _ _ _ => T
    | _ => F``
val it =
   ⊢ (pmatch t of Black _ _ _ => T | _ => F) ⇔
     case t of Empty => F | Red t a t0 => F | Black t' a' t0' => T: thm

Compiling to nchotomy theorems

Compiling to decision tree based case expressions is sometimes handy. However, computing the patterns corresponding to this decision tree is even more useful, since this set of patterns has very interesting properties. It is exhaustive and for each input pattern each pattern in this set is either a subpattern of the input pattern or distinct. There are no partial overlaps. Even better, whether an output pattern is a subpattern of an input pattern is checkable via simple first order matching.

Let's look at an example. First we compile a case expression to a decision tree.

> PMATCH_CASE_SPLIT_CONV ``pmatch xy of
  | (SOME x, SOME y) => x + y
  | (_, SOME 0) => 0``
metis: r[+0+5]+0+0+1+0+0#
val it =
   ⊢ (pmatch xy of (SOME x,SOME y) => x + y | (_,SOME 0) => 0) =
     case xy of
       (v,NONE) => ARB
     | (NONE,SOME 0) => 0
     | (NONE,SOME (SUC n)) => ARB
     | (SOME x'',SOME x') => x'' + x': thm

We end up with 4 rows in the pretty-printed form of the decision tree case expression. These 4 output patterns have the desired properties. They are exhaustive and for example (NONE, SOME 0) is a subpattern of (_, SOME 0), but distinct from (SOME x, SOME y). The nchotomy_of_pats function compiles the list of input patterns to an nchotomy-theorem containing exactly these 4 patterns.

> nchotomy_of_pats [``\(x,y). (SOME (x:num), SOME (y:num))``,
                 ``\(xo:num option). (xo, SOME 0)``]
val it =
   ⊢ ∀x. (∃v0. x = (v0,NONE)) ∨ x = (NONE,SOME 0) ∨
         (∃v4. x = (NONE,SOME (SUC v4))) ∨ (∃v3. x = (SOME v3,SOME 0)) ∨
         ∃v3 v5. x = (SOME v3,SOME (SUC v5)): thm

Such nchotomy theorems are very useful for finding missing patterns, detecting redundant rows and proving exhaustiveness. Essentially, one just removes one input pattern after the other by applying first order matching. The patterns that remain are not covered by the input.

Removing Redundant Rows

Using pattern compilation, it is straightforward to implement advanced redundancy checks. The conversion PMATCH_REMOVE_REDUNDANT_CONV and the corresponding simpset fragment PMATCH_REMOVE_REDUNDANT_ss are able to remove row 5 of the example already discussed in Section 8.9.1.4.

> PMATCH_REMOVE_REDUNDANT_CONV ``pmatch xy of
  | (SOME x, y) => 1 | (SOME 2, 3) => 2
  | (NONE, y) => 3 | (NONE, y) => 4
  | (x, 5) => 5``
metis: r[+0+5]+0+0+0+2+2+0+0+1+0+2#
metis: r[+0+7]+0+0+0+0+0+2+2+2+2+2+2+0+0+0+0+1+0+1+0+1+1+0+1+1#
val it =
   ⊢ (pmatch xy of
       (SOME x,y) => 1
     | (SOME 2,3) => 2
     | (NONE,y) => 3
     | (NONE,y) => 4
     | (x,5) => 5) =
     pmatch xy of (SOME x,y) => 1 | (NONE,y) => 3: thm

If the redundancy of a row depends not only on patterns, but also guards, the automated method often fails. Figure 8.9.7 shows an example, where the information that each natural number is either even or odd is needed to show that a row is redundant. In such situations, it is often beneficial to combine the automated redundancy removal technique with manual reasoning (as in the figure).

Figure 8.9.7. Manual reasoning about redundant rows.

val t = pmatch x of _ when EVEN x => 0 | _ when ODD x => 1 | _ => 2 ... output elided ... PMATCH_REMOVE_REDUNDANT_CONV t Exception- UNCHANGED raised

val info = COMPUTE_REDUNDANT_ROWS_INFO_OF_PMATCH t val info = ⊢ IS_REDUNDANT_ROWS_INFO x [PMATCH_ROW (λ_0. _0) (λ_0. EVEN x) (λ_0. 0); PMATCH_ROW (λ_0. _0) (λ_0. ODD x) (λ_0. 1); PMATCH_ROW (λ_0. _0) (λ_0. T) (λ_0. 2)] F [¬∃_0. x = _0 ∧ EVEN x; (∃v0. x = v0 ∧ ¬EVEN x) ⇒ ¬∃_0. x = _0 ∧ ODD x; (∃v0. x = v0 ∧ ¬EVEN x ∧ ¬ODD x) ⇒ ¬∃_0. x = _0]: thm

IS_REDUNDANT_ROWS_INFO_SHOW_ROW_IS_REDUNDANT_set_goal info 2 val it = Proof manager status: 4 proofs.

  1. Incomplete goalstack: Initial goal: x = 10 ∧ x < 16

    Current goal: x = 10

  2. Incomplete goalstack: Initial goal: (∀x xs x' xs'. SNOC x xs = SNOC x' xs' ⇔ x = x' ∧ xs = xs') ∧ ((∀x xs. [] ≠ SNOC x xs) ∧ ∀x xs. SNOC x xs ≠ []) ∧ (∀ff x. ff x = list_REVCASE x (ff []) (λx' xs. ff (SNOC x' xs))) ∧ (∀x' f1 f2 x f1' f2'. x' = x ⇒ (x = [] ⇒ f1 = f1') ⇒ (∀x' xs. x = SNOC x' xs ⇒ f2 x' xs = f2' x' xs) ⇒ list_REVCASE x' f1 f2 = list_REVCASE x f1' f2') ∧ ∀x. x = [] ∨ ∃x' xs. x = SNOC x' xs

  3. Incomplete goalstack: Initial goal: (∃v0. x = v0 ∧ ¬EVEN x ∧ ¬ODD x) ⇒ ¬∃_0. x = _0

  4. Incomplete goalstack: Initial goal: (∃v0. x = v0 ∧ ¬EVEN x ∧ ¬ODD x) ⇒ ¬∃_0. x = _0

> val info' = IS_REDUNDANT_ROWS_INFO_SHOW_ROW_IS_REDUNDANT info 2 ...
val info' =
   ⊢ IS_REDUNDANT_ROWS_INFO x
       [PMATCH_ROW (λ_0. _0) (λ_0. EVEN x) (λ_0. 0);
        PMATCH_ROW (λ_0. _0) (λ_0. ODD x) (λ_0. 1);
        PMATCH_ROW (λ_0. _0) (λ_0. T) (λ_0. 2)] F
       [¬∃_0. x = _0 ∧ EVEN x;
        (∃v0. x = v0 ∧ ¬EVEN x) ⇒ ¬∃_0. x = _0 ∧ ODD x; T]: thm

> val thm = IS_REDUNDANT_ROWS_INFO_TO_PMATCH_EQ_THM info'
val thm =
   ⊢ (pmatch x of _ when EVEN x => 0 | _ when ODD x => 1 | _ => 2) =
     pmatch x of _ when EVEN x => 0 | _ when ODD x => 1: thm

Pattern Match Completion

The techniques used for computing redundant rows implicitly compute a set of missing patterns. The conversion PMATCH_COMPLETE_CONV and simpset fragment PMATCH_COMPLETE_ss use this implicitly computed information to extend case expressions with ARB rows and thereby produce exhaustive \pmatch{} case expressions. A flag determines whether these newly introduced rows should use guards.

> PMATCH_COMPLETE_CONV true ``pmatch (xy : (num option # num option)) of
      (SOME x, NONE) when x > 0 => 0 | (NONE, _) => 1``;
val it =
   ⊢ (pmatch xy of (SOME x,NONE) when x > 0 => 0 | (NONE,_) => 1) =
     pmatch xy of
       (SOME x,NONE) when x > 0 => 0
     | (NONE,_) => 1
     | (SOME v2,NONE) when ¬(v2 > 0) => ARB
     | (SOME v2,SOME v3) => ARB: thm
> PMATCH_COMPLETE_CONV false ``pmatch (xy : (num option # num option)) of
      (SOME x, NONE) when x > 0 => 0 | (NONE, _) => 1``;
val it =
   ⊢ (pmatch xy of (SOME x,NONE) when x > 0 => 0 | (NONE,_) => 1) =
     pmatch xy of
       (SOME x,NONE) when x > 0 => 0
     | (NONE,_) => 1
     | (SOME v2,NONE) => ARB
     | (SOME v2,SOME v3) => ARB: thm

Exhaustiveness Checks

Similarly, exhaustiveness can be derived via pattern compilation.

> PMATCH_IS_EXHAUSTIVE_COMPILE_CHECK
    ``pmatch (xy : (num option # num option)) of
      (SOME _, _) => 0 | (_, NONE) => 1 | (NONE, SOME _) => 2``
val it =
   ⊢ PMATCH_IS_EXHAUSTIVE xy
       [PMATCH_ROW (λ(_0,_1). (SOME _0,_1)) (λ(_0,_1). T) (λ(_0,_1). 0);
        PMATCH_ROW (λ_0. (_0,NONE)) (λ_0. T) (λ_0. 1);
        PMATCH_ROW (λ_0. (NONE,SOME _0)) (λ_0. T) (λ_0. 2)] ⇔ T: thm

Often, the exhaustiveness can be proved much faster by just searching a matching row.

> PMATCH_IS_EXHAUSTIVE_FAST_CHECK ``pmatch (x:num option, y:num) of
      (SOME _, _) => 0 | (_, _) => 1``
val it =
   ⊢ PMATCH_IS_EXHAUSTIVE (x,y)
       [PMATCH_ROW (λ(_0,_1). (SOME _0,_1)) (λ(_0,_1). T) (λ(_0,_1). 0);
        PMATCH_ROW (λ(_0,_1). (_0,_1)) (λ(_0,_1). T) (λ(_0,_1). 1)] ⇔ T: thm

> PMATCH_IS_EXHAUSTIVE_FAST_CHECK ``pmatch (xy : (num option # num option)) of
     (SOME _, _) => 0 | (_, NONE) => 1 | (NONE, SOME _) => 2``
Exception- UNCHANGED raised

Both methods are combined to from PMATCH_IS_EXHAUSTIVE_CHECK.

Another interface to the pattern compilation engine is provided by SHOW_NCHOTOMY_CONSEQ_CONV. Exhaustiveness is this time expressed in the form of an nchotomy theorem. Missing cases are automatically added.

> SHOW_NCHOTOMY_CONSEQ_CONV
    ``!x:'a list. (x = []) \/ (?e. x = [e]) \/ (?e1 e2 l. x = e1::e2::l)``
val it = ⊢ ∀x. T ⇒ x = [] ∨ (∃e. x = [e]) ∨ ∃e1 e2 l. x = e1::e2::l: thm
> SHOW_NCHOTOMY_CONSEQ_CONV
    ``!x:'a list. (x = []) \/ (?e1 e2 l. x = e1::e2::l)``
val it = ⊢ ∀x. ¬(∃v1. x = [v1]) ⇒ x = [] ∨ ∃e1 e2 l. x = e1::e2::l: thm

Code Extraction

There is support for \pmatch{} case expressions in EmitML. However, not all case expressions are supported. Supported case expressions may only contain constructor patterns and each pattern variable needs to be used exactly once. Moreover, when extracting to SML, no guards are allowed.

To check whether a case expression can be exported, the function analyse_pmatch can be used. The flag of this function indicates whether an exhaustiveness proof should be attempted.

> val info = analyse_pmatch false
  ``pmatch l of [] => 1 | [x] when (x > 2) => 2 | _ => 3``
val info =
   {pmi_exhaustiveness_cond = NONE, pmi_has_double_bound_pat_vars = [],
    pmi_has_free_pat_vars = [], pmi_has_guards = [1], pmi_has_lambda_in_pat =
    [], pmi_has_non_contr_in_pat = [], pmi_has_unused_pat_vars = [],
    pmi_ill_formed_rows = [], pmi_is_well_formed = true}: pmatch_info

> val sml_ok = is_sml_pmatch info
val sml_ok = false: bool
> val ocaml_ok = is_ocaml_pmatch info
val ocaml_ok = true: bool

The HolSat Library

The HolSat Library

The purpose of HolSatLib is to provide a platform for experimenting with combinations of theorem proving and SAT solvers. Only black box functionality is provided at the moment; an incremental interface is not available.

HolSatLib provides a function SAT_PROVE for propositional satisfiability testing and for proving propositional tautologies. It uses an external SAT solver (currently MiniSat 1.14p) to find an unsatisfiability proof or satisfying assignment, and then reconstructs the proof or checks the assignment deductively in HOL.

Alternatively, the function SAT_ORACLE has the same behaviour as SAT_PROVE but asserts the result of the solver without proof. The theorem thus asserted is tagged with "HolSatLib" to indicate that it is unchecked. Since proof reconstruction can be expensive, the oracle facility can be useful during prototyping, or if proof is not required.

The following example illustrates the use of HolSatLib for proving propositional tautologies:

> load "HolSatLib"; open HolSatLib;   ... output elided ...

> show_tags := true;
val it = (): unit

> SAT_PROVE ``(a ==> b) /\ (b ==> a) <=> (a=b)``;
val it =
   [oracles: DISK_THM] [axioms: ] [] ⊢ (a ⇒ b) ∧ (b ⇒ a) ⇔ (a ⇔ b):
   thm

> SAT_PROVE ``(a ==> b) ==> (a=b)``
  handle HolSatLib.SAT_cex th => th;
val it =
   [oracles: DISK_THM] [axioms: ] [] ⊢ ¬a ∧ b ⇒ ¬((a ⇒ b) ⇒ (a ⇔ b)):
   thm

> SAT_ORACLE ``(a ==> b) /\ (b ==> a) <=> (a=b)``;
val it =
   [oracles: DISK_THM, HolSatLib] [axioms: ] []
   ⊢ (a ⇒ b) ∧ (b ⇒ a) ⇔ (a ⇔ b): thm

Setting show_tags to true makes the HOL top-level print theorem tags. The DISK_THM oracle tag has nothing to do with HolSatLib. It just indicates the use of theorems from HOL libraries read in from permanent storage.

Note that in the case where the putative tautology has a falsifying interpretation, a counter-model can be obtained by capturing the special exception SAT_cex, which contains a theorem asserting the counter-model.

The next example illustrates using HolSatLib for satisfiability testing. The idea is to negate the target term before passing it to HolSatLib.

> SAT_PROVE ``~((a ==> b) ==> (a=b))``
  handle HolSatLib.SAT_cex th => th;
val it =
   [oracles: DISK_THM] [axioms: ] [] ⊢ a ∧ ¬b ⇒ ¬¬((a ⇒ b) ⇒ (a ⇔ b)):
   thm

> SAT_PROVE ``~(a /\ ~a)``;
val it = [oracles: DISK_THM] [axioms: ] [] ⊢ ¬(a ∧ ¬a): thm

As expected, if the target term is unsatisfiable we get a theorem saying as much.

HolSatLib can only handle purely propositional terms (atoms must be propositional variables or constants) involving the usual propositional connectives as well as Boolean-valued conditionals. If you wish to prove tautologies that are instantiations of propositional terms, use tautLib (see Section 8.11.1 below).

If MiniSat failed to build when HOL was built, or proof replay fails for some other reason, SAT_PROVE falls back to a DPLL-based propositional tautology prover implemented in SML, due to Michael Norrish (see the HOL Tutorial). HolSatLib prints out a warning if this happens. On problems with more than a thousand or so clauses (in conjunctive normal form (CNF)), the SML prover will likely take too long to be of any use.

HolSatLib will delete temporary files generated by the SAT solver, such as the proof file and any statistics. This is to avoid accumulating thousands of possibly large files. Currently HolSatLib has only been tested on Linux, and on Windows XP using MinGW.

tautLib

tautLib predates HolSatLib by over a decade. It used a Boolean case analysis algorithm suggested by Tom Melham and implemented by R. J. Boulton. This algorithm has since been superseded and the functions in the tautLib signature now act as wrappers around calls to HolSatLib. However, the wrappers are able to provide the following extra functionality on top of HolSatLib:

  1. They can handle top level universal quantifiers.
  2. They can reason about (the propositional structure of) terms that are instances of purely propositional terms. This is done by a preprocessing step that replaces each unique instantiation with a fresh propositional variable.

For details, see the source file src/taut/tautLib.sml which contains comprehensive comments. Note however that the extra functionality in tautLib was not engineered for very large problems and can become a performance bottleneck.

Support for other SAT solvers

The ZChaff SAT solver has a proof production mode and is supported by HolSatLib. However, the ZChaff end user license is not compatible with the HOL license, so we are unable to distribute it with HOL. If you wish to use ZChaff, download and unpack it in the directory src/HolSat/sat_solvers/ under the main HOL directory, and compile it with proof production mode enabled (which is not the default). This should create a binary zchaff in the directory src/HolSat/sat_solvers/zchaff/. ZChaff can now be used as the external proof engine instead of MiniSat, by using the HolSatLib functions described above, prefixed with a "Z", e.g., ZSAT_PROVE.

A file resolve_trace may be created in the current working directory, when working with ZChaff. This is the proof trace file produced by ZChaff, and is hardwired.

Other SAT solvers are currently not supported. If you would like such support to be added for your favourite solver, please send a feature request via https://github.com/HOL-Theorem-Prover/HOL.

The general interface

The functions described above are wrappers for the function GEN_SAT, which is the single entry point for HolSatLib. GEN_SAT can be used directly if more flexibility is required. GEN_SAT takes a single argument, of type sat_config, defined in satConfig.sml. This is an opaque record type, currently containing the following fields:

  1. term : Term.term

    The input term.

  2. solver : SatSolvers.sat_solver

    The external SAT solver to use. The default is SatSolvers.minisatp. If ZChaff is installed (see Section 8.11.2), then SatSolvers.zchaff may also be used.

  3. infile : string option

    The name of a file in DIMACS format.1 Overrides term if set. The input term is instead read from the file.

  4. proof : string option

    The name of a proof trace file. Overrides solver if set. The file must be in the native format of HolSatLib, and must correspond to a proof for infile, which must also be set. The included version of MiniSat has been modified to produce proofs in the native format, and ZChaff proofs are translated to this format using the included proof translator src/HolSat/sat_solvers/zc2hs (type zc2hs -h for usage help). zc2hs is used internally by ZSAT_PROVE etc.

  5. is_cnf : bool

    If true then the input term is expected to be a negated CNF term. This is set automatically if infile is set. Typically a user will never need to modify this field directly.

  6. is_proved : bool

    If true then HOL will prove the SAT solver's results.

A special value base_config : sat_config is provided for which the term is T, the solver is MiniSat, the options are unset, the CNF flag is false and the proof flag is true. This value can be inspected and modified using getter and setter functions provided in src/HolSat/satConfig.sig. For example, to invoke ZChaff (assuming it is installed), on a file zchaff.cnf containing a DIMACS-formatted problem, we do:

> open satConfig;   ... output elided ...

> val c = base_config |> set_infile "zchaff.cnf"
                      |> set_solver SatSolvers.zchaff;
val c =
   {flags = {is_cnf = true, is_proved = true}, infile = SOME "zchaff.cnf",
    proof = NONE, pterm = “T”, solver =
    SatSolver
     {URL = "http://www.princeton.edu/~chaff/zchaff", end_string =
      "Random Seed Used", executable =
      "/Users/michaeln/HOL/src/HolSat/sat_solvers/zchaff/zchaff",
      failure_string = "UNSAT", name = "zchaff", notime_run = fn, only_true =
      false, post_exe =
      SOME "/Users/michaeln/HOL/src/HolSat/sat_solvers/zc2hs/zc2hs",
      post_run = fn, start_string = "Instance Satisfiable", time_run = fn}}:
   sat_config

> GEN_SAT c;
Exception- SAT_cex
  ⊢ v1 ∧ v5 ∧ v4 ∧ v3 ⇒
    ¬((v1 ∨ ¬v5 ∨ v4) ∧ (¬v1 ∨ v5 ∨ v3 ∨ v4) ∧ (¬v3 ∨ ¬v4)) raised

Normally, HolSatLib will delete the files generated by the SAT solver, such as the output proof, counter-model, and result status. However, if infile is set, the files are not deleted, in case they are required elsewhere.

Notes

On Linux and MacOS, g++ must be installed on the system for MiniSat and zc2hs to build.

Temporary files are generated using the Moscow ML function FileSys.tmpName. This usually writes to the standard temporary file space on the operating system. If that file space is full, or if it is inaccessible for some other reason, HolSatLib calls may fail mysteriously.

The function dimacsTools.readDimacs file reads a DIMACS format file and returns a CNF HOL term corresponding to the SAT problem in the file named by file. Since DIMACS uses numbers to denote variables, and numbers are not legal identifiers in HOL, each variable number is prefixed with the string "v". This string is defined in the reference variable dimacsTools.prefix and can be changed if required. This function can be used independently of HolSatLib to read in DIMACS format files.


  1. http://www.satlib.org/Benchmarks/SAT/satformat.ps

The HolQbf Library

The HolQbf Library

HolQbfLib provides a rudimentary platform for experimenting with combinations of theorem proving and Quantified Boolean Formulae (QBF) solvers. HolQbfLib was developed as part of a research project on Expressive Multi-theory Reasoning for Interactive Verification (EPSRC grant EP/F067909/1) from 2008 to 2011. It is loosely inspired by HolSatLib (Section 8.11), and has been described in parts in the following publications:

  • Tjark Weber: Validating QBF Invalidity in HOL4. In Matt Kaufmann and Lawrence C. Paulson, editors, Interactive Theorem Proving, First International Conference, ITP 2010, Edinburgh, UK, July 11–14, 2010. Proceedings, volume 6172 of Lecture Notes in Computer Science, pages 466–480. Springer, 2010.
  • Ramana Kumar and Tjark Weber: Validating QBF Validity in HOL4. In Marko C. J. D. van Eekelen, Herman Geuvers, Julien Schmaltz, and Freek Wiedijk, editors, Interactive Theorem Proving, Second International Conference, ITP 2011, Berg en Dal, The Netherlands, August 22–25, 2011. Proceedings, volume 6898 of Lecture Notes in Computer Science, pages 168–183. Springer, 2011.

HolQbfLib uses an external QBF solver, Squolem, to decide Quantified Boolean Formulae.

Installing Squolem

HolQbfLib has been tested with (the x86 Linux version of) Squolem 2.02 (release date 2010-11-10). This is Squolem's latest version at the time of writing. Squolem can be obtained from http://www.cprover.org/qbv/download.html. After installation, you must make the executable available as squolem2, e.g., by placing it into a folder that is in your $PATH. This name is currently hard-coded: there is no configuration option to tell HOL about the location and name of the Squolem executable.

Interface

The library provides four functions, each of type term -> thm, to invoke Squolem: decide, decide_prenex, disprove, and prove. These are defined in the HolQbfLib structure, which is the library's main entry point.

Calling prove $\phi$ will invoke Squolem on the QBF $\phi$ to establish its validity. If this succeeds, prove will then validate the certificate of validity generated by Squolem in HOL to return a theorem $\vdash \phi$.

Similarly, calling disprove $\phi$ will invoke Squolem to establish that $\phi$ is invalid. If this succeeds, disprove will then validate the certificate of invalidity generated by Squolem in HOL to return a theorem $\phi \vdash \bot$.

decide_prenex $\phi$ combines the functionality of prove and disprove into a single function. It will invoke Squolem on $\phi$ and return either $\vdash \phi$ or $\phi \vdash \bot$, depending on Squolem's answer.

Finally, decide does the same job as decide_prenex but accepts QBFs in a less restricted form. Restrictions on $\phi$ are described below.

Figure: HolQbfLib in action.

- load "HolQbfLib";
metis: r[+0+3]#
r[+0+6]#
> val it = () : unit

- open HolQbfLib;
> val decide = fn: term -> thm
val decide_prenex = fn: term -> thm
val disprove = fn: term -> thm
val prove = fn: term -> thm

- show_assums := true;
> val it = () : unit

- decide ``?x. x``;
<<HOL message: HolQbfLib: calling external command
  'squolem2 -c /tmp/filedH1K2x >/dev/null 2>&1'>>
> val it =  [] |- ?x. x: thm

- decide ``(?y. x \/ y) ==> ~x``;
> val it = [!x. (?y. x \/ y) ==> ~x] |- F: thm

- decide ``~(?x. x ==> y) \/ (?x. y ==> x)``;
<<HOL message: HolQbfLib: calling external command
  'squolem2 -c /tmp/fileyap3oD >/dev/null 2>&1'>>
> val it = [] |- ~(?x. x ==> y) \/ ?x. y ==> x: thm

- decide_prenex ``!x. ?y. x /\ y``;
<<HOL message: HolQbfLib: calling external command
  'squolem2 -c /tmp/fileZAGj4m >/dev/null 2>&1'>>
> val it = [!x. ?y. x /\ y] |- F : thm

- disprove ``!x. ?y. x /\ y``;
<<HOL message: HolQbfLib: calling external command
  'squolem2 -c /tmp/file0Pw2Tg >/dev/null 2>&1'>>
> val it = [!x. ?y. x /\ y] |- F : thm

- prove ``?x. x``;
<<HOL message: HolQbfLib: calling external command
  'squolem2 -c /tmp/fileKi4Lkz >/dev/null 2>&1'>>
- val it =  [] |- ?x. x: thm

Supported subset of higher-order logic

The argument given to decide must be a Boolean term built using only conjunction, disjunction, implication, negation, universal/existential quantification, and variables. Free variables are considered universally quantified. Every quantified variable must occur.

The argument given to the other functions must be a QBF in prenex form, i.e., a term of the form $Q_1 x_1. \, Q_2 x_2. \, \ldots \, Q_n x_n. \, \phi$, where

  • $n \geq 0$,
  • each $Q_i$ is an (existential or universal) quantifier,
  • $Q_n$ is the existential quantifier,
  • each $x_i$ is a Boolean variable,
  • $\phi$ is a propositional formula in CNF, i.e., a conjunction of disjunctions of (possibly negated) Boolean variables,
  • $\phi$ must actually contain each $x_i$,
  • all $x_i$ must be distinct, and
  • $\phi$ does not contain variables other than $x_1, \ldots, x_n$.

The behavior is undefined if any of these restrictions are violated.

Support for the QDIMACS file format

The QDIMACS standard defines an input file format for QBF solvers. HolQbfLib provides a structure QDimacs that implements (parts of) the QDIMACS standard, version 1.1 (released on December 21, 2005), as described at http://www.qbflib.org/qdimacs.html. The QDimacs structure does not require Squolem (or any other QBF solver) to be installed.

QDimacs.write_qdimacs_file path $\phi$ creates a QDIMACS file (with name path) that encodes the QBF $\phi$, where $\phi$ must meet the requirements detailed above. The function returns a dictionary that maps each variable in $\phi$ to its corresponding variable index (a positive integer) used in the QDIMACS file.

QDimacs.read_qdimacs_file f path parses an existing QDIMACS file (with name path) and returns the encoded QBF as a HOL term. Since variables are only given as integers in the QDIMACS format, variables in HOL are obtained by applying f (which is a function of type int -> term) to each integer. f is expected to return Boolean variables only, not arbitrary HOL terms.

Tracing

Tracing output can be controlled via Feedback.set_trace "HolQbfLib". See the source code in QbfTrace.sml for possible values.

Communication between HOL and Squolem is via temporary files. These files are located in the standard temporary directory, typically /tmp on Unix machines. The actual file names are generated at run-time, and can be shown by setting the above tracing variable to a sufficiently high value.

The default behavior of HolQbfLib is to delete temporary files after successful invocation of Squolem. This also can be changed via the above tracing variable. If there is an error, files are retained in any case (but note that the operating system may delete temporary files automatically, e.g., when HOL exits).

Wishlist

The following features have not been implemented yet. Please submit additional feature requests (or code contributions) via http://github.com/HOL-Theorem-Prover/HOL.

Support for other QBF solvers

So far, Squolem is the only QBF solver that has been integrated with HOL. Several other QBF solvers can produce proofs, and it would be nice to offer HOL users more choice (also because Squolem's performance is not necessarily state-of-the-art anymore).

QBF solvers as a web service

The need to install a QBF solver locally poses an entry barrier. It would be much more convenient to have a web server running one (or several) QBF solvers, roughly similar to the “System on TPTP” interface that G. Sutcliffe provides for first-order theorem provers (http://www.cs.miami.edu/~tptp/cgi-bin/SystemOnTPTP).

The HolSmt Library

The HolSmt Library

The purpose of HolSmtLib is to provide a platform for experimenting with combinations of interactive theorem proving and Satisfiability Modulo Theories (SMT) solvers. HolSmtLib was developed as part of a research project on Expressive Multi-theory Reasoning for Interactive Verification (EPSRC grant EP/F067909/1) from 2008 to 2011. It is loosely inspired by HolSatLib (Section 8.11), and has been described in parts in the following publications:

  • Tjark Weber: SMT Solvers: New Oracles for the HOL Theorem Prover. To appear in International Journal on Software Tools for Technology Transfer (STTT), 2011.
  • Sascha Böhme, Tjark Weber: Fast LCF-Style Proof Reconstruction for Z3. In Matt Kaufmann and Lawrence C. Paulson, editors, Interactive Theorem Proving, First International Conference, ITP 2010, Edinburgh, UK, July 11–14, 2010. Proceedings, volume 6172 of Lecture Notes in Computer Science, pages 179–194. Springer, 2010.

HolSmtLib uses external SMT solvers to prove instances of SMT tautologies, i.e., formulas that are provable using (a combination of) propositional logic, equality reasoning, linear arithmetic on integers and reals, and decision procedures for bit vectors and arrays. The supported fragment of higher-order logic varies with the SMT solver used, and is discussed in more detail below. At least for Yices, it is a superset of the fragment supported by bossLib.DECIDE (and the performance of HolSmtLib, especially on big problems, should be much better).

Interface

The library currently provides four tactics to invoke different SMT solvers, namely CVC_ORACLE_TAC, YICES_TAC, Z3_ORACLE_TAC, and Z3_TAC. These tactics are defined in the HolSmtLib structure, which is the library's main entry point. Given a goal $(\Gamma, \varphi)$ (where $\Gamma$ is a list of assumptions, and $\varphi$ is the goal's conclusion), each tactic returns (i) an empty list of new goals, and (ii) a validation function that returns a theorem $\Gamma' \vdash \varphi$ (with $\Gamma' \subseteq \Gamma$). These tactics fail if the SMT solver cannot prove the goal.1 In other words, these tactics solve the goal (or fail). As with other tactics, Tactical.TAC_PROOF can be used to derive functions of type goal -> thm.

For each tactic, the HolSmtLib structure additionally provides a corresponding function of type term -> thm. These functions are called CVC_ORACLE_PROVE, YICES_PROVE, Z3_ORACLE_PROVE, and Z3_PROVE, respectively. Applied to a formula $\varphi$, they return the theorem $\emptyset \vdash \varphi$ (or fail).

Furthermore, the HolSmtLib structure provides three more tactics, namely cvco_tac, z3o_tac and z3_tac. These tactics are equivalent to CVC_ORACLE_TAC, Z3_ORACLE_TAC and Z3_TAC (respectively), but additionally take a list of theorems which are used as lemmas in the proof, similarly to METIS_TAC.

Oracles vs. proof reconstruction

CVC_ORACLE_TAC, YICES_TAC, Z3_ORACLE_TAC, cvco_tac and z3o_tac use the SMT solver (cvc5, Yices, Z3, cvc5 and Z3, respectively) as an oracle: the solver's result is trusted. Bugs in the SMT solver or in HolSmtLib could potentially lead to inconsistent theorems. Accordingly, the derived theorem is tagged with an oracle tag.

Z3_TAC and z3_tac, on the other hand, perform proof reconstruction. They request a detailed proof from Z3, which is then checked in HOL. One obtains a proper HOL theorem; no (additional) oracle tags are introduced. However, Z3's proofs do not always contain enough information to allow efficient checking in HOL; therefore, proof reconstruction may be slow or fail.

Supported subsets of higher-order logic

YICES_TAC employs a translation into Yices's native input format. The interface supports types bool, num, int, real, -> (i.e., function types), prod (i.e., tuples), fixed-width word types, inductive data types, records, and the following terms: equality, Boolean connectives (T, F, ==>, /\, \/, negation, if-then-else, bool-case), quantifiers (!, ?), numeric literals, arithmetic operators (SUC, +, -, *, /, unary minus, DIV, MOD, ABS, MIN, MAX), comparison operators (<, <=, >, >=, both on num, int, and real), function application, lambda abstraction, tuple selectors FST and SND, and various word operations.

cvc5 and Z3 are integrated via a more restrictive translation into SMT-LIB 2 format, described below. Therefore, Yices is typically the solver of choice at the moment (unless you need proof reconstruction, which is available for Z3 only). However, there are a few operations (e.g., specific word operations) that are supported by the SMT-LIB format, but not by Yices. See selftest.sml for further details.

Terms of higher-order logic that are not supported by the respective target solver / translation are typically treated in one of three ways:

  1. Some unsupported terms are replaced by equivalent supported terms during a pre-processing step. For instance, all tactics first generalize the goal's conclusion by stripping outermost universal quantifiers, and attempt to eliminate certain set expressions by rewriting them into predicate applications: e.g., y IN {x | P x} is replaced by P y. The resulting term is $\beta$-normalized. Depending on the target solver, further simplifications are performed.
  2. Remaining unsupported constants are treated as uninterpreted, i.e., replaced by fresh variables. This should not affect soundness, but it may render goals unprovable and lead to spurious counterexamples. To see all fresh variables introduced by the translation, you can set HolSmtLib's tracing variable (see below) to a sufficiently high value.
  3. Various syntactic side conditions are currently not enforced by the translation and may result in invalid input to the SMT solver. For instance, Yices only allows linear arithmetic (i.e., multiplication by constants) and word-shifts by numeric literals (constants). If the goal is outside the allowed syntactic fragment, the SMT solver will typically fail to decide the problem. HolSmtLib at present only provides a generic error message in this case. Inspecting the SMT solver's output might provide further hints.
- load "HolSmtLib"; open HolSmtLib;
(* output omitted *)
> val it = () : unit

- show_tags := true;
> val it = () : unit

- CVC_ORACLE_PROVE ``(a ==> b) /\ (b ==> a) <=> (a <=> b)``;
> val it = [oracles: DISK_THM, HolSmtLib] [axioms: ] []
           |- (a ==> b) /\ (b ==> a) <=> (a <=> b) : thm

- YICES_PROVE ``(a ==> b) /\ (b ==> a) <=> (a <=> b)``;
> val it = [oracles: DISK_THM, HolSmtLib] [axioms: ] []
           |- (a ==> b) /\ (b ==> a) <=> (a <=> b) : thm

- Z3_ORACLE_PROVE ``(a ==> b) /\ (b ==> a) <=> (a <=> b)``;
> val it = [oracles: DISK_THM, HolSmtLib] [axioms: ] []
           |- (a ==> b) /\ (b ==> a) <=> (a <=> b) : thm

- Z3_PROVE ``(a ==> b) /\ (b ==> a) <=> (a <=> b)``;
> val it = [oracles: DISK_THM] [axioms: ] []
           |- (a ==> b) /\ (b ==> a) <=> (a <=> b) : thm

Support for the SMT-LIB 2 file format

SMT-LIB (see https://smtlib.cs.uiowa.edu/) is the standard input format for SMT solvers. HolSmtLib supports (a subset of) version 2.0 of this format. A translation of HOL terms into SMT-LIB 2 format is available in SmtLib.sml, and a parser for SMT-LIB 2 files (translating them into HOL types, terms, and formulas) can be found in SmtLib_Parser.sml, with auxiliary functions in SmtLib_{Logics,Theories}.sml.

The SMT-LIB 2 translation supports types bool, num, int and real, fixed-width word types, and the following terms: equality, Boolean connectives, quantifiers, numeric literals, arithmetic operators, comparison operators, function application, and various word operations. Notably, the SMT-LIB interface does not support data types or records, and higher-order formulas. See the files mentioned above and the examples in selftest.sml for further details.

Tracing

Tracing output can be controlled via Feedback.set_trace "HolSmtLib". See the source code in Library.sml for possible values.

Communication between HOL and external SMT solvers is via temporary files. These files are located in the standard temporary directory, typically /tmp on Unix machines. The actual file names are generated at run-time, and can be shown by setting the above tracing variable to a sufficiently high value.

The default behavior of HolSmtLib is to delete temporary files after successful invocation of the SMT solver. This also can be changed via the above tracing variable. If there is an error, files are retained in any case (but note that the operating system may delete temporary files automatically, e.g., when HOL exits).

Installing SMT solvers

HolSmtLib has been tested with cvc5 1.3.0, Yices 1.0.40, and Z3 4.15.3. Later versions may or may not work. (Yices 2 is not supported.) To use HolSmtLib, you need to install at least one of these SMT solvers on your machine. As mentioned before, Yices supports a larger fragment of higher-order logic than Z3, but proof reconstruction has been implemented only for Z3.

cvc5 is available for various platforms from https://cvc5.github.io/. After installation, you must set the environment variable $HOL4_CVC_EXECUTABLE to the pathname of the cvc5 executable, e.g., /usr/bin/cvc5, before you invoke HOL.

Yices is available for various platforms from https://yices.csl.sri.com/. After installation, you must set the environment variable $HOL4_YICES_EXECUTABLE to the pathname of the Yices executable, e.g., /bin/yices, before you invoke HOL.

Z3 is available for various platforms from https://github.com/Z3Prover/z3. After installation, you must set the environment variable $HOL4_Z3_EXECUTABLE to the pathname of the Z3 executable, e.g., /bin/z3, before you invoke HOL.

It should be relatively straightforward to integrate other SMT solvers that support the SMT-LIB 2 input format as oracles. However, this will involve a (typically small) amount of Standard ML programming, e.g., to interpret the solver's output. See CVC.sml and Z3.sml for some relevant code.

Wishlist

The following features have not been implemented yet. Please submit additional feature requests (or code contributions) via https://github.com/HOL-Theorem-Prover/HOL.

Counterexamples

For satisfiable input formulas, SMT solvers typically return a satisfying assignment. This assignment could be displayed to the HOL user as a counterexample. It could also be turned into a theorem, similar to the way HolSatLib treats satisfying assignments.

Proof reconstruction

Several other SMT solvers can also produce proofs, and it would be nice to offer HOL users more choice. However, in the absence of a standard proof format for SMT solvers, it is perhaps not worth the implementation effort.

Support for cvc5's and Z3's SMT-LIB extensions

cvc5 and Z3 support extensions of the SMT-LIB language, e.g., data types, sets, multisets/bags and strings. HolSmtLib does not utilize these extensions yet when calling these SMT solvers. This would require the translation for these solvers to be distinct from the generic SMT-LIB translation.

SMT solvers as a web service

The need to install an SMT solver locally poses an entry barrier. It would be much more convenient to have a web server running one (or several) SMT solvers, roughly similar to the “System on TPTP” interface that G. Sutcliffe provides for first-order theorem provers (http://www.cs.miami.edu/~tptp/cgi-bin/SystemOnTPTP). For Isabelle/HOL, such a web service has been installed by S. Böhme in Munich, but unfortunately it is not publicly available. Perhaps the SMT-EXEC initiative (http://www.smtexec.org/) could offer hardware or implementation support.


  1. Internally, the goal's assumptions and the negated conclusion are passed to the SMT solver. If the SMT solver determines that these formulas are unsatisfiable, then the (unnegated) conclusion must be provable from the assumptions.

The Quantifier Heuristics Library

The Quantifier Heuristics Library

Motivation

Often interactive proofs can be simplified by instantiating quantifiers. The Unwind library, which is part of the simplifier, allows instantiations of “trivial” quantifiers: $$ \forall x_1\ \ldots x_i \ldots x_n.\ P_1 \wedge \ldots \wedge x_i = c \wedge \ldots \wedge P_n \Longrightarrow Q $$ and $$ \exists x_1\ \ldots x_i \ldots x_n.\ P_1 \wedge \ldots \wedge x_i = c \wedge \ldots \wedge P_n $$ can be simplified by instantiating $x_i$ with $c$. Because unwind-conversions are part of bool_ss, they are used with nearly every call of the simplifier and often simplify proofs considerably. However, the Unwind library can only handle these common cases. If the term structure is only slightly more complicated, it fails. For example, $\exists x.\ P(x) \Longrightarrow (x = 2) \wedge Q(x)$ cannot be tackled.

There is also the Satisfy library, which uses unification to show existentially quantified formulas. It can handle problems like $\exists x.\ P_1(x,c_1)\ \wedge \ldots P_n(x,c_n)$ if given theorems of the form $\forall x\ c.\ P_i(x, c)$. This is often handy, but still rather limited.

The quantifier heuristics library (quantHeuristicsLib) provides more power and flexibility. A few simple examples of what it can do are shown in Table 8.14.1. Besides the power demonstrated by these examples, the library is highly flexible as well. At its core, there is a modular, syntax driven search for instantiation. This search consists of a collection of interleaved heuristics. Users can easily configure existing heuristics and add own ones. Thereby, it is easy to teach the library about new predicates, logical connectives or datatypes.

Table: Examples.

ProblemResult
basic examples
$\exists x.\ x = 2 \wedge P (x)$$P(2)$
$\forall x.\ x = 2 \Longrightarrow P (x)$$P(2)$
solutions and counterexamples
$\exists x.\ x = 2$true
$\forall x.\ x = 2$false
complicated nestings of standard operators
$\exists x_1. \forall x_2.\ (x_1 = 2) \wedge P(x_1, x_2)$$\forall x_2.\ P(2, x_2)$
$\exists x_1, x_2.\ P_1(x_2) \Longrightarrow (x_1 = 2) \wedge P(x_1, x_2)$$\exists x_2.\ P_1(x_2) \Longrightarrow P(2, x_2)$
$\exists x.\ ((x = 2) \vee (2 = x)) \wedge P(x)$$P(2)$
exploiting unification
$\exists x.\ (f (8 + 2) = f (x + 2)) \wedge P (f(10))$$P (f(10))$
$\exists x.\ (f (8 + 2) = f (x + 2)) \wedge P (f(x + 2))$$P (f(8 + 2))$
$\exists x.\ (f (8 + 2) = f (x + 2)) \wedge P (f(x))$— (no instantiation found)
partial instantiation for datatypes
$\forall p.\ c = \mathsf{FST}(p) \Longrightarrow P(p)$$\forall p_2.\ P(c, p_2)$
$\forall x.\ \mathsf{IS\_NONE}(x) \vee P(x)$$\forall x'.\ P (\mathsf{SOME}(x'))$
$\forall l.\ l \neq [\,] \Longrightarrow P(l)$$\forall \mathit{hd}, \mathit{tl}.\ P(\mathit{hd} :: \mathit{tl})$
context
$P_1(c) \Longrightarrow \exists x.\ P_1(x) \vee P_2(x)$true
$P_1(c) \Longrightarrow \forall x.\ \neg P_1(x) \wedge P_2(x)$$\neg P_1(c)$
$(\forall x.\ P_1(x) \Rightarrow (x = 2)) \Longrightarrow (\forall x.\ P_1(x) \Rightarrow P_2(x))$$(\forall x.\ P_1(x) \Rightarrow (x = 2)) \Rightarrow (P_1(2) \Rightarrow P_2(2))$
$\big((\forall x.\ P_1(x) \Rightarrow P_2(x)) \wedge P_1(2)\big) \Longrightarrow \exists x.\ P_2(x)$true

User Interface

The quantifier heuristics library can be found in the sub-directory src/quantHeuristics. The entry point to the framework is the library quantHeuristicsLib.

Conversions

Usually the library is used for converting a term containing quantifiers to an equivalent one. For this, the following high-level entry points exists:

Entry pointType
QUANT_INSTANTIATE_CONVquant_param list -> conv
QUANT_INST_ssquant_param list -> ssfrag
QUANT_INSTANTIATE_TACquant_param list -> tactic
ASM_QUANT_INSTANTIATE_TACquant_param list -> tactic

All these functions get a list of quantifier heuristic parameters as arguments. These parameters essentially configure which heuristics are used during the guess-search. If an empty list is provided, the tools know about the standard Boolean combinators, equations and context. std_qp adds support for common datatypes like pairs or lists. Quantifier heuristic parameters are explained in more detail in Section 8.14.4.

So, some simple usage of the quantifier heuristic library looks like:

- QUANT_INSTANTIATE_CONV [] ``?x. (!z. Q z /\ (x=7)) /\ P x``;
> val it = |- (?x. (!z. Q z /\ (x = 7)) /\ P x) <=> (!z. Q z) /\ P 7: thm

- QUANT_INSTANTIATE_CONV [std_qp] ``!x. IS_SOME x ==> P x``
> val it = |- (!x. IS_SOME x ==> P x) <=> !x_x'. P (SOME x_x'): thm

Usually, the quantifier heuristics library is used together with the simplifier using QUANT_INST_ss. Besides interleaving simplification and quantifier instantiation, this has the benefit of being able to use context information collected by the simplifier:

- QUANT_INSTANTIATE_CONV [] ``P m ==> ?n. P n``
Exception- UNCHANGED raised

- SIMP_CONV (bool_ss ++ QUANT_INST_ss []) [] ``P m ==> ?n. P n``
> val it = |- P m ==> (?n. P n) <=> T: thm

It's usually best to use QUANT_INST_ss together with e.g. SIMP_TAC when using the library with tactics. However, if free variables of the goal should be instantiated, then ASM_QUANT_INSTANTIATE_TAC should be used:

P x
------------------------------------
  IS_SOME x
  : proof

- e (ASM_QUANT_INSTANTIATE_TAC [std_qp])
> P (SOME x_x') : proof

There is also QUANT_INSTANTIATE_TAC. This tactic does not instantiate free variables. Neither does it take assumptions into consideration. It is just a shortcut for using QUANT_INSTANTIATE_CONV as a tactic.

Unjustified Guesses

Most heuristics justify the guesses they produce and therefore allow to prove equivalences of e.g. the form $\exists x.\ P(x) \Leftrightarrow P(i)$. However, the implementation also supports unjustified guesses, which may be bogus. Let's consider e.g. the formula $\exists x.\ P(x) \Longrightarrow (x = 2)\ \wedge\ Q(x)$. Because nothing is known about $P$ and $Q$, we can't find a safe instantiation for $x$ here. However, $2$ looks tempting and is probably sensible in many situations. (Counterexample: $P(2)$, $\neg Q(2)$ and $\neg P(3)$ hold.)

implication_concl_qp is a quantifier parameter that looks for valid guesses in the conclusion of an implication. Then, it assumes without justification that these guesses are probably sensible for the whole implication as well. Because these guesses might be wrong, one can either use implications or expansion theorems like $\exists x.\ P(x)\ \Longleftrightarrow (\forall x.\ x \neg c \Rightarrow \neg P(x)) \Rightarrow P(c)$.

- QUANT_INSTANTIATE_CONV [implication_concl_qp]
     ``?x. P x ==> (x = 2) /\ Q x``
Exception- UNCHANGED raised

- QUANT_INSTANTIATE_CONSEQ_CONV [implication_concl_qp]
     CONSEQ_CONV_STRENGTHEN_direction
     ``?x. P x ==> (x = 2) /\ Q x``
> val it =
   |- (P 2 ==> Q 2) ==> ?x. P x ==> (x = 2) /\ Q x: thm

- EXPAND_QUANT_INSTANTIATE_CONV [implication_concl_qp]
    ``?x. P x ==> (x = 2) /\ Q x``
> val it = |- (?x. P x ==> (x = 2) /\ Q x) <=>
              (!x. x <> 2 ==> ~(P x ==> (x = 2) /\ Q 2)) ==> P 2 ==> Q 2

- SIMP_CONV (std_ss++EXPAND_QUANT_INST_ss [implication_concl_qp]) []
    ``?x. P x ==> (x = 2) /\ Q x``
> val it =
   |- (?x. P x ==> (x = 2) /\ Q x) <=>
      (!x. x <> 2 ==> P x) ==> P 2 ==> Q 2: thm

The following entry points should be used to exploit unjustified guesses:

Entry pointType
QUANT_INSTANTIATE_CONSEQ_CONVquant_param list -> directed_conseq_conv
EXPAND_QUANT_INSTANTIATE_CONVquant_param list -> conv
EXPAND_QUANT_INST_ssquant_param list -> ssfrag
QUANT_INSTANTIATE_CONSEQ_TACquant_param list -> tactic

Explicit Instantiations

A special (degenerated) use of the framework is turning guess search off completely and providing instantiations explicitly. The tactic QUANT_TAC allows this. This means that it allows to partially instantiate quantifiers at subpositions with explicitly given terms. As such, it can be seen as a generalisation of EXISTS_TAC.

- val it = !x. (!z. P x z) ==> ?a b.    Q a        b z : proof

> e( QUANT_INST_TAC [("z", `0`, []), ("a", `SUC a'`, [`a'`])] )
- val it = !x. (    P x 0) ==> ?  b a'. Q (SUC a') b z : proof

This tactic is implemented using unjustified guesses. It normally produces implications, which is fine when used as a tactic. There is also a conversion called INST_QUANT_CONV with the same functionality. For a conversion, implications are problematic. Therefore, the simplifier and Metis are used to prove the validity of the explicitly given instantiations. This succeeds only for simple examples.

Simple Quantifier Heuristics

The full quantifier heuristics described above are powerful and very flexible. However, they are sometimes slow. The unwind library1 on the other hand is limited, but fast. The simple version of the quantifier heuristics fills the gap in the middle. They just search for gap guesses without any free variables. Moreover, slow operations like recombining or automatically looking up datatype information is omitted. As a result, the conversion SIMPLE_QUANT_INSTANTIATE_CONV (and corresponding SIMPLE_QUANT_INST_ss) is nearly as fast as the corresponding unwind conversions. However, it supports more complicated syntax. Moreover, there is support for quantifiers, pairs, list and much more.

Quantifier Heuristic Parameters

Quantifier heuristic parameters play a similar role for the quantifier instantiation library as simpsets do for the simplifier. They contain theorems, ML code and general configuration parameters that allow to configure guess-search. There are predefined parameters that handle common constructs and the user can define own parameters.

Quantifier Heuristic Parameters for Common Datatypes

There are option_qp, list_qp, num_qp and sum_qp for option types, lists, natural numbers and sum types respectively. Some examples are displayed in the following table:

$$ \begin{array}{r@{\quad \Longleftrightarrow \quad}l} \forall x.\ \holtxt{IS\_SOME}(x) \Rightarrow P(x) & \forall x'.\ P (\holtxt{SOME}(x')) \\ \forall x.\ \holtxt{IS\_NONE}(x)& \textit{false} \\ \forall l.\ l \neq [\,] \Rightarrow P(l)& \forall h, l'.\ P(h::l') \\ \forall x.\ x = c + 3& \textit{false} \\ \forall x.\ x \neq 0 \Rightarrow P(x)& \forall x'.\ P(\holtxt{SUC}(x')) \end{array} $$

Quantifier Heuristic Parameters for Tuples

For tuples the situation is peculiar, because each quantifier over a variable of a product type can be instantiated. The challenge is to decide which quantifiers should be instantiated and which new variable names to use for the components of the pair.

There is a quantifier heuristic parameter called pair_default_qp. It first looks for subterms of the form $(\lambda (x_1, \ldots, x_n).\ \ldots)\ x$. If such a term is found $x$ is instantiated with $(x_1, \ldots, x_n)$. Otherwise, subterms of the form $\holtxt{FST}(x)$ and $\holtxt{SND}(x)$ are searched. If such a term is found, $x$ is instantiated as well. This parameter therefore allows simplifications like:

$$ \begin{array}{r@{\quad \Longleftrightarrow \quad}l} \forall p.\ (x = \holtxt{SND}(p)) \Rightarrow P(p)& \forall p_1.\ P(p_1, x) \\ \exists p.\ (\lambda (p_a, p_b, p_c). P(p_a, p_b, p_c))\ p & \exists p_a, p_b, p_c.\ P(p_a, p_b, p_c) \end{array} $$

pair_default_qp is implemented in terms of the more general quantifier heuristic parameter pair_qp, which allows the user to provide a list of ML functions. These functions get the variable and the term. If they return a tuple of variables, these variables are used for the instantiation, otherwise the next function in the list is called or — if there is no function left — the variable is not instantiated. In the example of $\exists p.\ (\lambda (p_a, p_b, p_c). P(p_a, p_b, p_c))\ p$ these functions are given the variable $p$ and the term $(\lambda (p_a, p_b, p_c). P(p_a, p_b, p_c))\ p$ and return $\holtxt{SOME} (p_a, p_b, p_c)$. This simple ML interface gives the user full control over what quantifier over product types to expand and how to name the new variables.

Quantifier Heuristic Parameter for Records

Records are similar to pairs, because they can always be instantiated. Here, it is interesting that the necessary monochotomy lemma comes from HOL 4's Type_Base library. This means that record_qp is stateful. If a new record type is defined, the automatically proven monochotomy lemma is then automatically used by record_qp. In contrast to the pair parameter, the one for records gets only one function instead of a list of functions to decide which variables to instantiate. However, this function is simpler, because it just needs to return true or false. The names of the new variables are constructed from the field-names of the record. The quantifier heuristic parameter default_record_qp expands all records.

Stateful Quantifier Heuristic Parameters

The parameter for records is stateful, as it uses knowledge from Type_Base. Such information is not only useful for records but for general datatypes. The quantifier heuristic parameter TypeBase_qp uses automatically proven theorems about new datatypes to exploit mono- and dichotomies. Moreover, there is also a stateful pure_stateful_qp that allows the user to explicitly add other parameters to it. stateful_qp is a combination of pure_stateful_qp and TypeBase_qp.

Standard Quantifier Heuristic Parameter

The standard quantifier heuristic parameter std_qp combines the parameters for lists, options, natural numbers, the default one for pairs and the default one for records.

User defined Quantifier Heuristic Parameters

The user is also able to define own parameters. There is empty_qp, which does not contain any information. Several parameters can be combined using combine_qps. Together with the basic types of user defined parameters that are explained below, these functions provide an interface for user defined quantifier heuristic parameters.

Rewrites / Conversions

A very powerful, yet simple technique for teaching the guess search about new constructs are rewrite rules. For example, the standard rules for equations and basic logical operations cannot generate guesses for the predicate IS_SOME. By rewriting IS_SOME(x) to ?x'. x = SOME(x'), however, these rules fire.

option_qp uses this rewrite to implement support for IS_SOME. Similarly support for predicates like NULL is implemented using rewrites. Even adding rewrites like $\mathsf{append}(l_1, l_2) = [\,] \Longleftrightarrow (l_1 = [\,]\ \wedge\ l_2 = [\,])$ for list-append turned out to be beneficial in practice.

rewrite_qp allows to provide rewrites in the form of rewrite theorems. For the example of IS_SOME this looks like:

> val thm = QUANT_INSTANTIATE_CONV [] ``!x. IS_SOME x ==> P x``
Exception- UNCHANGED raised

> val IS_SOME_EXISTS = prove (``IS_SOME x = (?x'. x = SOME x')``,
   Cases_on `x` THEN SIMP_TAC std_ss []);
val IS_SOME_EXISTS = |- IS_SOME x <=> ?x'. x = SOME x': thm

> val thm = QUANT_INSTANTIATE_CONV [rewrite_qp[IS_SOME_EXISTS]]
    ``!x. IS_SOME x ==> P x``
val thm = |- (!x. IS_SOME x ==> P x) <=>
             !x'. IS_SOME (SOME x') ==> P (SOME x'): thm

To clean up the result after instantiation, theorems used to rewrite the result after instantiation can be provided via final_rewrite_qp.

> val thm = QUANT_INSTANTIATE_CONV [rewrite_qp[IS_SOME_EXISTS],
                                    final_rewrite_qp[option_CLAUSES]]
      ``!x. IS_SOME x ==> P x``
val thm = |- (!x. IS_SOME x ==> P x) <=> !x'. P (SOME x'): thm

If rewrites are not enough, conv_qp can be used to add conversions:

- val thm = QUANT_INSTANTIATE_CONV [] ``?x. (\y. y = 2) x``
Exception- UNCHANGED raised

- val thm = QUANT_INSTANTIATE_CONV [convs_qp[BETA_CONV]] ``?x. (\y. y = 2) x``
> val thm = |- (?x. (\y. y = 2) x) <=> T: thm

Strengthening / Weakening

In rare cases, equivalences that can be used for rewrites are unavailable. There might be just implications that can be used for strengthening or weakening. The function imp_qp might be used to provide such implication.

- val thm = QUANT_INSTANTIATE_CONV [list_qp] ``!l. 0 < LENGTH l ==> P l``
Exception- UNCHANGED raised

- val LENGTH_LESS_IMP = prove (``!l n. n < LENGTH l ==> l <> []``,
    Cases_on `l` THEN SIMP_TAC list_ss []);
> val LENGTH_LESS_IMP = |- !l n. n < LENGTH l ==> l <> []: thm

- val thm = QUANT_INSTANTIATE_CONV [imp_qp[LENGTH_LESS_IMP], list_qp]
    ``!l. 0 < LENGTH l ==> P l``
> val thm =
   |- (!l. 0 < LENGTH l ==> P l) <=>
      !l_t l_h. 0 < LENGTH (l_h::l_t) ==> P (l_h::l_t): thm

- val thm = SIMP_CONV (list_ss ++
              QUANT_INST_ss [imp_qp[LENGTH_LESS_IMP], list_qp]) []
              ``!l. SUC (SUC n) < LENGTH l ==> P l``
> val thm =
   |- (!l. SUC (SUC n) < LENGTH l ==> P l) <=>
      !l_h l_t_h l_t_t_t l_t_t_h. n < SUC (LENGTH l_t_t_t) ==>
                                  P (l_h::l_t_h::l_t_t_h::l_t_t_t): thm

Filtering

Sometimes, one might want to avoid to instantiate certain quantifiers. The function filter_qp allows to add ML-functions that filter the handled quantifiers. These functions are given a variable $x$ and a term $P(x)$. The tool only tries to instantiate $x$ in $P(x)$, if all filter functions return true.

- val thm = QUANT_INSTANTIATE_CONV []
     ``?x y z. (x = 1) /\ (y = 2) /\ (z = 3) /\ P (x, y, z)``
> val thm = |- (?x y z. (x = 1) /\ (y = 2) /\ (z = 3) /\ P (x,y,z)) <=>
               P (1,2,3): thm

- val thm = QUANT_INSTANTIATE_CONV
     [filter_qp [fn v => fn t => (v = ``y:num``)]]
     ``?x y z. (x = 1) /\ (y = 2) /\ (z = 3) /\ P (x, y, z)``
> val thm = |- (?x y z. (x = 1) /\ (y = 2) /\ (z = 3) /\ P (x,y,z)) <=>
                ?x   z. (x = 1) /\            (z = 3) /\ P (x,2,z): thm

Satisfying and Contradicting Instantiations

As the satisfy library demonstrates, it is often useful to use unification and explicitly given theorems to find instantiations. In addition to satisfying instantiations, the quantifier heuristics framework is also able to use contradicting ones. The theorems used for finding instantiations usually come from the context. However, instantiation_qp allows to add additional ones:

> val thm = SIMP_CONV (std_ss++QUANT_INST_ss[]) []
    ``P n ==> ?m:num. n <= m /\ P m``
Exception- UNCHANGED raised

> val thm = SIMP_CONV (std_ss++
               QUANT_INST_ss[instantiation_qp[LESS_EQ_REFL]]) []
               ``P n ==> ?m:num. n <= m /\ P m``
> val thm = |- P n ==> ?m:num. n <= m /\ P m = T : thm

Di- and Monochotomies

Dichotomies can be exploited for guess search. distinct_qp provides an interface to add theorems of the form $\forall x.\ c_1(x) \neq c_2(x)$. cases_qp expects theorems of the form $\forall x. \ (x = \exists \mathit{fv}. c_1(\mathit{fv}))\ \vee \ldots \vee (x = \exists \mathit{fv}. c_n(\mathit{fv}))$. However, only theorems for $n = 2$ and $n = 1$ are used. All other cases are currently ignored.

Oracle Guesses

Sometimes, the user does not want to justify guesses. The tactic QUANT_TAC is implemented using oracle guesses for example. A simple interface to oracle guesses is provided by oracle_qp. It expects a ML function that given a variable and a term returns a pair of an instantiation and the free variables in this instantiation.

As an example, let's define a parameter that states that every list is non-empty:

   val dummy_list_qp = oracle_qp (fn v => fn t =>
     let
        val (v_name, v_list_ty) = dest_var v;
        val v_ty = listSyntax.dest_list_type v_list_ty;

        val x = mk_var (v_name ^ "_hd", v_ty);
        val xs = mk_var (v_name ^ "_tl", v_list_ty);
        val x_xs = listSyntax.mk_cons (x, xs)
     in
        SOME (x_xs, [x, xs])
     end)

Notice, that an option type is returned and that the function is allowed to throw HOL_ERR exceptions. With this definition, we get

- NORE_QUANT_INSTANTIATE_CONSEQ_CONV [dummy_list_qp]
    CONSEQ_CONV_STRENGTHEN_direction ``?x:'a list y:'b. P (x, y)``
> val it = ?y x_hd x_tl. P (x_hd::x_tl,y)) ==> ?x y. P (x,y) : thm

Lifting Theorems

The function inference_qp enables the user to provide theorems that allow lifting guesses over user defined connectives. As writing these lifting theorems requires deep knowledge about guesses, it is not discussed here. Please have a look at the detailed documentation of the quantifier heuristics library as well as its sources. You might also want to contact Thomas Tuerk (tt291@cl.cam.ac.uk).

User defined Quantifier Heuristics

At the lowest level, the tool searches guesses using ML-functions called quantifier heuristics. Slightly simplified, such a quantifier heuristic gets a variable and a term and returns a set of guesses for this variable and term. Heuristics allow full flexibility. However, to write your own heuristics a lot of knowledge about the ML-datastructures and auxiliary functions is required. Therefore, no details are discussed here. Please have a look at the source code and contact Thomas Tuerk (tt291@cl.cam.ac.uk), if you have questions. heuristics_qp and top_heuristics_qp provide interfaces to add user defined heuristics to a quantifier heuristics parameter.


  1. see src/simp/src/Unwind.sml

Tree-Structured Finite Sets and Finite Maps

Tree-Structured Finite Sets and Finite Maps

The source files behind this library are found in src/finite_maps. There are theories, generated by script files (with Script.sml suffixes): toto (total orders mapping pairs of objects into a ternary ordering type), enumeral (sets as binary trees), tc (transitive closure calculation via Warshall's algorithm) and fmapal (tree-based finite-map representation); along with supporting library files (with suffixes .sig and .sml): totoTacs, tcTacs, fmapalTacs, enumTacs. The library was written by F. Lockwood Morris.

For any type ty that has been equipped with a total order and a conversion for evaluating it, new terms of type ty set are provided which embody minimum-depth binary search trees. The primary objective has been to supply an IN_CONV for such terms with running time logarithmic in the cost of a single order comparison together with additional set operations which have reasonable running times.

Similarly, for ty as above and any type ty', new terms of type ty |-> ty' embody binary search trees, and enable a logarithmic-time FAPPLY_CONV and various other operations on finite maps.

Total orders: the type 'a toto

The use of binary search trees naturally requires that a total order be supplied for whatever type ty of elements [arguments] the sets [finite maps] are to have. Rather than the relation type ty -> ty -> bool, it is found computationally advantageous to use the type ty -> ty -> cpn, where cpn is the HOL datatype of three elements LESS, EQUAL, GREATER. A polymorphic defined type, 'a toto, has been created isomorphic to the class of functions : 'a -> 'a -> cpn satisfying a predicate totoTheory.TotOrd which axiomatizes total order-hood. The representation function for the type is called apto (for “apply total order”). Specifically what is needed in order to use ty as an element [argument] type is (the name of) an element of type ty toto, say tyto, and a conversion, say tyto_CONV, that will reduce terms of the form apto tyto x y to one of LESS, EQUAL, GREATER.

Provided in totoTheory are orders numto, intto, charto, stringto, and qk_numto (the last is an unnatural order on type num that should in principle be quicker to compute on NUMERAL terms than the usual order) with corresponding conversions numto_CONV, etc. Also, for lexicographic order on pairs, there are the object language function lextoto : 'a toto -> 'b toto -> ('a#'b)toto and the ML function lextoto_CONV : conv -> conv -> conv; if cva and cvb are conversions for evaluating terms that start apto toa ... and apto tob ... respectively, lextoto_CONV cva cvb is a conversion for evaluating terms starting apto (toa lextoto tob) ... . Similarly, there are listoto : 'a toto -> 'a list toto and listoto_CONV : conv -> conv. Inspection of listoto and listoto_CONV, possibly also of qk_numto and qk_numto_CONV, should make it feasible to define orders directly as toto's and corresponding conversions as needed for other HOL datatypes.

Additionally, given any linear order R : ty -> ty -> bool, if one supplies the theorem and definition

        lin_ord_thm:  |- LinearOrder $R
        toto_of_dfn:  |- cmp = toto_of_LinearOrder $R

and two conversions, say eq_conv for reducing equations of ground terms (of R's argument type) to T or to F, and lo_conv for reducing terms t R t' to T or to F, then

        toto_CONV lin_ord_thm toto_of_dfn eq_conv lo_conv

is a corresponding conversion for evaluating terms of the form apto cmp c c'.

Interpretation of binary trees as sets; IN_CONV

The datatype

        bt = nt | node of 'a bt => 'a => 'a bt

is defined with the objective of forming terms ENUMERAL cmp b, where cmp is a ty toto and b is a ty bt. These should justify the pair of theorems

⊢ ∀ cmp y. y ∈ ENUMERAL cmp nt ⇔ F

and

⊢ ∀ cmp x l y r.
     x ∈ ENUMERAL cmp (node l y r) ⇔
     case apto cmp x y of
       LESS  ⇒ x ∈ ENUMERAL cmp l
     | EQUAL ⇒ T
     | GREATER ⇒ x ∈ ENUMERAL cmp r

To make these theorems come out true requires the following definition of ENUMERAL:

⊢ (∀ cmp. ENUMERAL cmp nt = {}) ∧
   ∀ cmp l x r.
     ENUMERAL cmp (node l x r) =
     {y | y ∈ ENUMERAL cmp l ∧ (apto cmp y x = LESS)} ∪ {x} ∪
     {z | z ∈ ENUMERAL cmp r ∧ (apto cmp x z = LESS)} .

Invoking IN_CONV keyconv ``x IN ENUMERAL cmp b```, where keyconvis a conversion for evaluating applications ofcmp, will convert the term to whichever truth value the definition of ENUMERALcompels; that is, toTif and only if top-down tree search discoversxinb. Operations that create ENUMERALterms, discussed below, will ensure thatbis a well-formed binary search tree of minimal depth. (The newIN_CONV, if it is given an equality-deciding conversion and a set built with INSERTrather than atoto-evaluating conversion and an ENUMERALset, will revert topred_setLib.IN_CONV`.)

Translating between set representations

HOL offers two notations for explicit finite sets: the display notation {x1; ...; xn}, which is short for x1 INSERT ... INSERT xn INSERT {}, and notation with an explicit list: set [x1; ...; xn]. We provide here conversions back and forth between these:

        DISPLAY_TO_set_CONV: conv
        set_TO_DISPLAY_CONV: conv

and to create ENUMERAL sets from either:

        set_TO_ENUMERAL_CONV: conv -> term -> conv
        DISPLAY_TO_ENUMERAL_CONV: conv -> term -> conv

(these demanding a toto order on the element type and a conversion for evaluating it), and to recover either from an ENUMERAL:

        ENUMERAL_TO_set_CONV: conv -> conv
        ENUMERAL_TO_DISPLAY_CONV: conv -> conv

requiring only the order-evaluating conversion. Additionally,

        TO_set_CONV: conv -> conv

will normalize any of the three forms to the set [ ... ] form. (NO_CONV will suffice as the conversion argument if it is not an ENUMERAL that is to be normalized.) The conversions from the ENUMERAL form have to execute, for a well-formed $n$-element binary search tree, $n-1$ comparisons to verify that all the tree elements belong to the represented set; they will also succeed, in conformance with the definition of ENUMERAL, for ill-formed trees, if such are ever created, at the cost of between $2n$ and $3n$ comparisons.

Creation of an ENUMERAL form from either of the others entails sorting, performed by an $n\log n$ list-merging algorithm, followed by a detour through another datatype, 'a bl, which could as well have been ('a # 'a bt) option list, where the list element of index $k$, if present, consists of a full binary tree of $2^k - 1$ set elements and one more, which may be thought of as a root of which the full tree is the right subtree. A sorted linear list is copied into a bl by successive "BL_CONS" operations imitating the incrementation of a binary counter; when the copy is complete, it is collapsed into a single bt of which the biggest constituent full bt is indeed the right subtree. The upshot is that going from an ordered linear list to the ENUMERAL form is a linear-time operation, and that the bt in any ENUMERAL set has a unique shape for its size: that of a minimal-depth tree with all right subtrees full as one proceeds down the left spine.

Binary operations on sets

The functions

        UNION_CONV: conv -> conv
        INTER_CONV: conv -> conv
        SET_DIFF_CONV: conv -> conv

will work out applications of UNION, INTER, DIFF respectively to two ENUMERAL sets, given a conversion to evaluate the relevant order. UNION_CONV will revert to pred_setLib.UNION_CONV if that is what fits its arguments.

These operations work by list merging, hence with a linear number of comparisons. The strategy is to convert each input, say ENUMERAL cmp b, to a theorem,

        |- OWL cmp (ENUMERAL cmp b) l

asserting that it could have been created with set_TO_ENUMERAL_CONV from a certain list, which moreover is in strict ascending order:

      OWL
        |- !cmp s l. OWL cmp s l <=> (s = set l) /\ OL cmp l
      OL
        |- (!cmp. OL cmp [] <=> T) /\
           !cmp a l. OL cmp (a::l) <=>
                     OL cmp l /\ !p. MEM p l ==> (apto cmp a p = LESS) .

Underlying conversions OWL_UNION, OWL_INTER, OWL_DIFF, each of type conv -> thm -> thm -> thm, combine two such theorems to produce a third; the result, known to be ordered, is retransformed into an ENUMERAL term without further comparisons, as in the post-sorting steps of set_TO_ENUMERAL_CONV.

Explicit translations

        OWL_TO_ENUMERAL: thm -> thm
        ENUMERAL_TO_OWL: conv -> term -> thm
        set_TO_OWL: conv -> term -> term -> thm

between ENUMERAL terms and OWL theorems permit OWL_UNION, etc. to be invoked directly. (set_TO_OWL allows to create an OWL theorem from either a set [ ... ] term or a { ... } term without first making an ENUMERAL.)

In addition, there is

        SET_EXPR_CONV: conv -> conv

which, given a conversion to evaluate the order cmp, will work out the value of any set expression built up with UNION, INTER, and DIFF from ENUMERAL terms, avoiding intermediate translations.

Interpretation of binary trees as finite maps; FAPPLY_CONV

The treatment of finite maps parallels that of sets: the new terms denoting maps of type ty |-> ty' are terms FMAPAL cmp b, where cmp is a ty toto and b is a (ty#ty') bt. The theorems supporting tree search are

⊢ ∀ cmp x. FMAPAL cmp nt ' x = FEMPTY ' x

and

⊢ ∀ cmp x l a b r.
     FMAPAL cmp (node l (a,b) r) ' x =
     case apto cmp x a of
       LESS  ⇒ FMAPAL cmp l ' x
     | EQUAL ⇒ b
     | GREATER ⇒ FMAPAL cmp r ' x ,

and the definition of FMAPAL is

⊢ (∀ cmp. FMAPAL cmp nt = FEMPTY) ∧
   ∀ x v r l cmp.
     FMAPAL cmp (node l (x,v) r) =
     DRESTRICT (FMAPAL cmp l) {y | apto cmp y x = LESS} FUNION
     FEMPTY |+ (x,v) FUNION
     DRESTRICT (FMAPAL cmp r) {z | apto cmp x z = LESS} .

An invocation FAPPLY_CONV keyconv ``(FMAPAL cmp b) ' x```, where keyconvis a conversion to evaluate applications ofcmp, will yield the value paired with xifxis in the domain ofFMAPAL cmp b, or `` ``FEMPTY ' x`` `` if xis not to be found. IfFAPPLY_CONVis instead given an equality-deciding conversion and a termfmap [ ... ] ' x, it will produce the first value paired with x` in the list if any, FEMPTY ' x if none.

Translating between finite map representations

A compact list representation for finite maps is defined:

      fmap
        |- !l. fmap l = FEMPTY |++ REVERSE l

The point of the reversal is that l is now treated as an association list — in case of duplicated arguments, the pair nearest the front of the list will take precedence. Translating between fmap [ ... ] and FMAPAL cmp ... terms for finite maps we have

        fmap_TO_FMAPAL_CONV: conv -> term -> conv
        FMAPAL_TO_fmap_CONV: conv -> conv

the first of which demands the name of a toto-evaluating function as well as a conversion for computing its value.

Given a term FUN_FMAP f (set [ ... ]) and a conversion f_conv for working out applications of f,

        FUN_fmap_CONV: conv -> conv

will convert the term to the form fmap [ ... ], and

        FUN_FMAPAL_CONV: conv -> term -> conv -> conv,

which expects the conversion and term arguments to fmap_TO_FMAPAL_CONV followed by the conversion argument for FUN_fmap_CONV, will apply the latter and then the former to yield a FMAPAL term.

Binary operations involving finite maps

Parallel to the treatment of sets, a theorem representation of a finite map FMAPAL cmp b, namely

        ORWL cmp (FMAPAL cmp b) l,

asserting that it corresponds to a certain ordered list, is used for operations involving merging:

      ORWL
        |- !cmp f l. ORWL cmp f l <=> (f = fmap l) /\ ORL cmp l
      ORL
        |- (!cmp. ORL cmp [] <=> T) /\
           !l cmp b a. ORL cmp ((a,b)::l) <=>
                 ORL cmp l /\ !p q. MEM (p,q) l ==> (apto cmp a p = LESS) .

Functions

        FMAPAL_TO_ORWL: conv -> term -> thm
        ORWL_TO_FMAPAL: thm -> thm

translate between FMAPAL terms and ORWL theorems, and the latter can be produced directly from fmap [ ... ] terms by

        fmap_TO_ORWL: conv -> term -> term -> thm

which might find use, independent of finite maps, as a list sorting routine.

The only binary operation on FMAPAL terms is

        FUNION_CONV: conv -> conv

which will convert a term FUNION cmp (FMAPAL cmp b) (FMAPAL cmp b') to a FMAPAL term denoting a map defined on the union of the two domains, with the first argument map taking precedence where these overlap. But there are also two forms of domain restriction:

        DRESTRICT f s
        DRESTRICT f (COMP s)

for f a FMAPAL term and s an ENUMERAL term with the same order as f. A single conversion

        DRESTRICT_CONV: conv -> conv

will work out either of these forms to a FMAPAL result.

Like UNION_CONV, INTER_CONV, SET_DIFF_CONV, both FUNION_CONV and DRESTRICT_CONV entail two preliminary computations of FMAPAL_TO_ORWL or ENUMERAL_TO_OWL, a list-merging working part, one of

        ORWL_FUNION: conv -> thm -> thm -> thm
        ORWL_DRESTRICT: conv -> thm -> thm -> thm
        ORWL_DRESTRICT_COMPL: conv -> thm -> thm -> thm,

and a final use of ORWL_TO_FMAPAL. As with sets, the working parts may be used directly. Alternatively, translations between terms and theorems may be held to a minimum by the use of

        FMAP_EXPR_CONV: conv -> conv

on any expression built up with FUNION, DRESTRICT, and COMP from compatible FMAPAL and ENUMERAL terms.

Other operations on finite maps

The following operations on FMAPAL terms have no need to translate to the ORWL theorem form; two of them are unconcerned with the total ordering.

The conversion

        FDOM_CONV: conv

will reduce any term FDOM (FMAPAL ... ) to the isomorphic ENUMERAL ... , also any term FDOM (fmap [ ... ]) to set [ ... ].

The conversion-valued functon

        IN_FDOM_CONV: conv -> conv,

given a conversion to evaluate the applications of cmp, will reduce any term x IN FDOM (FMAPAL cmp b) to a truth value, or if given an equality-deciding conversion, it will reduce a term x IN FDOM (fmap [ ... ]) to a truth value.

The conversion-valued function

        o_f_CONV: conv -> conv,

given a conversion for working out applications of the function f, will reduce a term f o_f (FMAPAL ... ) to an isomorphic FMAPAL term, alternatively a term f o_f fmap [ ... ] to an isomorphic fmap term.

Similarly,

        FUPDATE_CONV: conv -> conv

expects either a cmp-evaluating conversion and a term FMAPAL cmp b |+ (x, y) or an equality-deciding conversion and a term fmap [ ... ] |+ (x, y), and if x is already in the domain of the finite map, will produce an isomorphic structure in which the value paired with x has been replaced by y. If x is not in the domain of the finite map, an error is reported.

It may be noted that FUN_FMAPAL_CONV, FAPPLY_CONV, and FUPDATE_CONV combine to provide a functional array facility with logarithmic number of index comparisons for both reading and writing.

An application: transitive closure

The idea of Warshall's algorithm for transitive closure of a relation, to build up an approximation to the answer by repeatedly allowing one fresh element as an intermediate stop in building a path between any two elements, is captured by the definition

subTC R s x y ⇔
  R x y ∨ ∃ a b. (R ^|^ s)* a b ∧ a ∈ s ∧ b ∈ s ∧ R x a ∧ R b y

where $\left(R\right.$ ^|^ $\left.s\right)^*$ denotes the reflexive transitive closure of the relation $R$ restricted fore and aft to the set $s$, and by the theorem (note that it is a set equation, as the Curried relations have been given only one argument)

⊢ ∀ R s x a. subTC R (x INSERT s) a =
     if x ∈ subTC R s a then subTC R s a ∪ subTC R s x else subTC R s a .

A representation of finite relations by set-valued finite maps is defined by

      FMAP_TO_RELN (f:'a |-> 'a set) x = if x IN FDOM f then f ' x else {}.

The conversion-valued function

        TC_CONV: conv -> conv,

given a conversion for evaluating cmp, will transform a term

        (FMAP_TO_RELN (FMAPAL cmp ... ))^+,

where the bt "..." stores (element, ENUMERAL) pairs, to the form FMAP_TO_RELN (FMAPAL cmp ...... ), or given an equality-deciding conversion, will turn

        (FMAP_TO_RELN (fmap [ ... ]))^+,

where now "..." is a list of elements paired with { ... } sets, into FMAP_TO_RELN (fmap [ ....... ]). It is because TC_CONV uses only the operations TO_set_CONV, IN_CONV, UNION_CONV, FDOM_CONV, and o_f_CONV that it is insensitive to which pair of representations is chosen for finite maps and sets.

As a convenience in preparing the tree-structured representation of a finite relation, the conversion

        ENUF_CONV: conv -> term -> conv,

given a conversion for evaluating a toto and its name, will convert a term fmap [ ... ] whose list members pair elements with either { ... } sets or set [ ... ] sets into a FMAPAL term with ENUMERAL values.

HOL Source File Syntax

Files written by HOL users are of the same form as those written by HOL developers. In this way, HOL aims to be easy to extend, and this chapter’s description of HOL source files should be useful for users and developers both.

HOL organises its files into two categories:

  • “script” files that specify a theory segment: where definitions are made, types defined and theorems proved; and

  • “library” files that provide SML implementations of important theorem-proving infrastructure, typically providing SML functions to support those actions done in script files (definitions, types, proofs).

Library files are written in SML, using the HOL API that is described both in this manual, but also in REFERENCE. Script files have their own rather special syntax that is a superset of SML. Script files will be the bulk of what most users write. Both flavours of file should observe the conventions documented in this chapter.

Library Files

A library is typically implemented in two SML files, Libname.sig and Libname.sml, where Libname.sig defines the SML signature for the library, and the corresponding .sml file provides the implementation. Conventionally, the stem of the file-name is also the name of both the signature and the structure. If our library is called “foo”, then foo.sig would have content:

   signature foo =
   sig
    …
   end

and foo.sml would contain

   structure foo :> foo =
   struct
    …
   end

Because of the link between the file-name and the SML structure’s name, the stem of the file-name will always be a valid SML identifier (ruling out hyphens, for example). It is permitted to omit the signature and to have no ascription in the .sml file. To support Moscow ML, when there is a signature, it is recommended to always use opaque ascription with the :> token.

By convention, a library written to support work with respect to a particular concept or type will often be called conceptLib. Thus, wordsLib supports work with the type of fixed-width words. Another common flavour of library is the so-called “syntax” library, designed to support working with terms and types related to these common syntaxes. Thus, HOL provides the library optionSyntax, where the SML function mk_some of type term -> term, builds a HOL term with the HOL constant SOME applied to the provided argument. Occasionally, a conceptLib library will import and re-export the contents of a linked conceptSyntax library.

Generally, library implementations should only provide entry-points to effect changes to the global state of the system; they should not directly make changes themselves. Logical content (definitions of constants, proofs of theorems, etc.) should always be prompted by user‐written function calls in script files. Changes to other aspects of the global state (e.g., the state of the grammar) should also only happen in the body of functions provided by the library, not as a side effect of the library being loaded.

Script Files

When writing a theory called name, the user must write a file with name nameScript.sml. This will ultimately generate an SML structure that users will be able to refer to as nameTheory. That generated structure will contain bindings for the user’s definitions and theorems. For example, HOL comes with a dividesScript.sml that when built allows:

> dividesTheory.prime_def;
val it = ⊢ ∀a. prime a ⇔ a ≠ 1 ∧ ∀b. divides b a ⇒ b = a ∨ b = 1: thm

The recommended syntax for a script file begins

   Theory <thyname>
   Ancestors <thy1> <thy2> ..
   Libs <lib1> <lib2> ..

   <Script Declarations>

The three keywords (Theory, Ancestors and Libs) must all appear in column 0 of the input file. The Ancestors and Libs components may be omitted.

The name following the Theory keyword must match the name that precedes the Script.sml of the file-name, and must be a valid SML structure identifier. In some cases, such as when a theory is huge and the theorems not particularly interesting, it is possible to disable the generation of documentation in <thyname>Theory.sig by following the theory name with the no_sig_docs annotation. The other annotation possible in this position is bare, which is used to indicate that the theory is built with the minimal hol --bare mode (and won't be using the facilities in bossLib, for example). We expect this modifier should only be necessary for developers.

If present, the names following Ancestors identify theories that this theory builds upon. These names should be “bare”: they cannot include the Theory suffix. By default, all of the SML identifiers within the exported theories mentioned in this section are available without qualification. Thus, if list is one of the names following Ancestors, one need not write listTheory.MAP in the rest of the file to refer to its theorem MAP, but can instead write simply MAP. If two ancestor theories use the same names for their exported theories, the names belonging to the theory whose name occurs later in the Ancestors list will shadow earlier occurrences.

It is reasonable and permitted to list a theory as an ancestor solely to be able to write its names without qualification. For example, one need not explicitly write pred_set as an ancestor because pred_setTheory is always included in HOL’s standard logical context. Nonetheless, one might mention it in order to be able to write IN_INSERT instead of pred_setTheory.IN_INSERT. Conversely, one might want a theory as an ancestor, but not to have its SML names available at the top-level. This effect is achieved by following the theory name with the [qualified] annotation.

In some situations, one wants to have a theory as an ancestor, but not to use its notations and other grammar changes. To achieve this, the ignore_grammar attribute can be used.

The names following the Libs keyword are the names of HOL libraries that the script-file depends on. These can also be imported in qualified fashion.

Finally, both ancestors and libraries can be given aliases for easy (qualified) access with the alias modifier. For example, if one writes Libs fooLib[alias=F], then fooLib's bindings are available in the SML structure F, allowing one to write F.entrypoint. Aliases can be used with or without the qualified option.

For convenience, the qualified and ignore_grammar options can be given “all at once” to a number of theory or library dependencies by attaching these attributes (again in square brackets) to the relevant Ancestors or Libs keyword directly. The alias=ident modifier does not make sense immediately after the Ancestors and Libs keywords; it should only be used after individual theory or library names to establish aliases for just those names.

Non-terminalProduction
theory::=Theorythyname⟩⟨build-mods?
(⟨ancestors⟩ | ⟨libraries⟩)*
thydecl*
build-mods::=[build-mod⟩ (,build-mod⟩)* ]
build-mod::=no_sig_docs    |    bare
ancestors::=Ancestorsthy-modifier?ancestor-spec*
libraries::=Libslib-modifier?lib-spec*
ancestor-spec::=thyname⟩ ⟨thy-modifier?
lib-spec::=thyname⟩ ⟨lib-modifier?
thy-modifier::=[thy-attr⟩(,thy-attr⟩)*]
lib-modifier::=[module-attr⟩(,module-attr⟩)*]
thy-attr::=module-attr⟩     |     ignore_grammar
module-attr::=qualified     |     alias=ident

Table: Script File Syntax. Square brackets in the table are input characters; they are not meta-characters (unlike the parentheses, pipe symbols and superscripts). All keywords beginning with capital letters (Theory, Ancestors, Libs, ...) must occur in column 0 of the input file.

Non-terminalProduction
thydecl::=SML-declaration⟩    |    ⟨theorem-decl⟩    |    ⟨definition-decl
| ⟨parsing-decl⟩    |    ⟨quotation-decl
theorem-decl::=thm-eqn⟩   |   ⟨thm-proof⟩   |   ⟨resumption⟩   |   ⟨final-thm
thm-eqn::=Theoremident⟩⟨thm-attrs=SML-expression
thm-proof::=Theoremident⟩⟨thm-attrs:thm-stmt
Proofproof-attrs⟩ ⟨tactic
QED
defn-decl::=datatype-decl⟩    |    ⟨function-decl⟩    |    ⟨relation-decl
datatype-decl::=Datatype:datatype-specEnd
relation-decl::=(Inductive|CoInductive) ⟨ident:relation-specEnd
function-decl::=Definitionident⟩⟨defn-attrs:
function-spec
(Terminationtactic⟩)? End
parsing-decl::=Overloadquoted-ident⟩⟨overload-attrs? =SML-expression
| Typequoted-ident⟩⟨type-attrs? =SML-expression
quotation-decl::=Quote (⟨ident=)?ident:quoted-materialEnd
resumption::=Resumeident⟩⟨resumption-attrs:tactic
QED
final-thm::=Finaliseident⟩⟨thm-attrs

Table: Syntax for expressing theorem proofs, definitions and other theory changes. Again, upper-case keywords (End in particular, despite the table layout), must occur in column 0.

Attributes

Many commands take a variety of attributes to modify or tweak the behaviour of the command. Attribute specifications are added to commands, usually after user-provided identifiers, within square brackets using a common syntax:

Non-terminalProduction
attr-spec::=[attribute⟩ (,attribute⟩)* ]
attribute::=ident⟩ (=ident+)?

If multiple identifiers appear to the right of an equals-sign in the above syntax, they are separated by whitespace. Libraries can enable more attributes than this core set for theorems and definitions; this allows theorem values to be stored in custom data-sets for later retrieval by customisable tools.

Keyword(Commonly used) Possible attributes
Definitionind=..., nocompute, schematic, simp
Overloadby_nametype, inferior, local
Proofexclude_frags, exclude_simps
Resumeexclude_frags, exclude_simps, smlname
Theorem (& Finalise)compute, cong, local, mono, rule_induction, simp
Typelocal, pp

In addition, the Resume command is required to include an attribute list that has as its first element the name/label for the particular sub-case that is to be proved in this resumption block.

The following describes the attributes listed above:

by_nametype

Used with Overload. This requires the SML-expression that appears after the =-sign in the syntax to be of type {Thy : string, Name : string} (normally this argument is a term), and maps to the underlying overload_on_by_nametype or temp_overload_on_by_nametype SML calls.

compute

Used with Theorem forms. This causes the theorem value to be added to the global “computation set” that is used by EVAL.

cong

Used with Theorem forms. This adds the corresponding theorem to the set of congruence rules used by the termination analysis behind function definition.

exclude_frags=...

Used with Proof or Resume. The list of fragment names following is removed from the global simpset for the duration of the given proof, affecting all simplifier tactics that refer to the global simpset. The exclusion is sticky: while it is in effect, attempts to add back a fragment with one of the named names via the ++ operator (including those performed implicitly by derived simpsets such as the one used by simp[]) are silent no-ops. To opt back in for a specific simplification call, use the SF marker in its theorem-list argument (e.g. simp[SF ARITH_ss] inside a Proof[exclude_frags = ARITH] body re-enables ARITH_ss for that one call).

exclude_simps=...

Used with Proof or Resume. The list of theorem names following is removed from the global simpset for the duration of the given proof. Names are not SML identifiers, but rather references to the names used internally by the simpset (visible by examining it with a call to srw_ss()), and can be given without theory-qualification. For example, with or without sumTheory’s identifiers visible to SML, one can use exclude_simps=ISR to stop the ISR rewrite from being used.

ind=ident

Used with Definition. Specifies the name for a recursive function's induction theorem.

inferior

Used with Overload. Makes the chosen overload “least liked” by the parsing and printing machinery. It will be chosen last when resolving ambiguous parses, and will only be used when printing if no other overload matches.

local

Used with Overload, Theorem, and Type. Makes the “effect” of the corresponding keyword local to the script file, meaning it will not persist into descendent theories. In the case of Theorem, this means the theorem will not be exported, and so it will not be possible to refer to it as fooTheory.ident in descendents. For the parsing directives, the changes to the grammar are not exported.

mono

Used with Theorem. Adds the theorem to the list of monotonicity results used by the inductive/coinductive definitions package.

nocompute

Used with Definition. This stops the definition theorem from being added to EVAL’s global “computation set”, which otherwise happens by default.

pp

Used with Type. Causes the new type abbreviation to be used when types are printed; by default, abbreviations are used only for parsing strings into types.

rule_induction

Used with Theorem. Makes the provided theorem available for use as a rule induction principle when using the Induct_on tactic.

schematic

Used with Definition. Allows schematic definitions.

simp

Used with Theorem and Definition. Adds the corresponding theorem to the global simpset. This effect will not persist beyond the current script file where it occurs if used with a local attribute.

smlname=⟨ident

Used with Resume. Gives the subresult corresponding to this theorem resumption the name ⟨ident⟩ in the SML namespace.

Miscellaneous Features

This section describes some of the features that exist for managing the interface to the HOL system.

  • The help system.
  • The trace system for controlling feedback and printing.
  • Holmake: a tool for dependency maintenance in large developments.
  • Functions for counting the number of primitive inferences done in an evaluation, and timing it.
  • A tool for embedding pretty-printed HOL theorems, terms and types in LaTeX documents.

Help

There are several kinds of help available in HOL, all accessible through the same incantation:

    help <string>;

The kinds of help available are:

Moscow ML help. (When using Moscow ML HOL) This is uniformly excellent. Information for library routines is available, whether the library is loaded or not via help "Lib".

HOL overview. This is a short summary of important information about HOL.

HOL help. This on-line help is intended to document all HOL-specific functions available to the user. It is very detailed and often accurate; however, it can be out-of-date, refer to earlier versions of the system, or even be missing!

HOL structure information. For most structures in the HOL source, one can get a listing of the entrypoints found in the accompanying signature. This is helpful for locating functions and is automatically derived from the system sources, so it is always up-to-date.

Theory facts. These are automatically derived from theory files, so they are always up-to-date. The signature of each theory is available (since theories are represented by structures in HOL). Also, each axiom, definition, and theorem in the theory can be accessed by name in the help system. As such theorems are pretty-printed into the corresponding Theory.sig file, the help system will find both the declaration in the signature (e.g., val nm :thm), and the entry for that theorem in the comment-block.

Therefore the following example queries can be made:

QueryResult
help "installPP"Moscow ML help
help "hol"HOL overview
help "aconv"on-line HOL help
help "Tactic"HOL source structure information
help "boolTheory"theory structure signature
help "list_Axiom"theory structure signature and theorem statement

The Trace System

The trace system gives the user one central interface with which to control most of HOL's many different flags, though they are scattered all over the system, and defined in different modules. These flags are typically those that determine the level to which HOL tools provide information to the user while operating. For example, a trace level of zero will usually make a tool remain completely silent while it operates. The tool may still raise an exception when it fails, but it won't also output any messages saying so.

There are three core functions, all in the Feedback structure:

   traces : unit ->
            {default: int, max: int, name: string, trace_level: int} list

   set_trace : string -> int -> unit
   trace     : (string * int) -> ('a -> 'b) -> ('a -> 'b)

The traces function returns a list of all the traces in the system. The set_trace function allows the user to set a trace directly. The effect of this might be seen in a subsequent call to traces(). Finally, the trace function allows for a trace to be temporarily set while a function executes, restoring the trace to its old value when the function returns (whether normally, or with an exception).

Maintaining HOL Formalizations with Holmake

The purpose of Holmake is to maintain dependencies in a HOL project (a collection of HOL files, possibly distributed over multiple directories). A single invocation of Holmake will compute dependencies between files, (re)compile plain ML code, (re)compile and execute theory scripts, and (re)compile the resulting theory modules. Holmake does not require the user to provide any explicit dependency information themselves. The conventions it relies on for source-file naming and ancestry declaration are described below.

Holmake can be accessed through

   <hol-dir>/bin/Holmake.

The development model that Holmake is designed to support is that there are two modes of work: theory construction and system revision. In ‘theory construction’ mode, the user builds up a theory by interacting with HOL, perhaps over many sessions. In ‘system rebuild’ mode, a component that others depend on has been altered, so all modules dependent on it have to be brought up to date. System rebuild mode is simpler so we deal with it first.

System rebuild

A system rebuild happens when an existing theory has been improved in some way (augmented with a new theorem, a change to a definition, etc.), or perhaps some support ML code has been modified or added to the formalization under development. The user needs to find and recompile just those modules affected by the change. This is what an invocation of Holmake does, by identifying the out-of-date modules and re-compiling and re-executing them.

Theory construction

A theory myTheory is constructed by writing the file myScript.sml. In this file, the context (semantic, and also proof support) is established, by specifying parent theories and useful libraries. In the course of building the theory, the user is writing what one might call the “HOL script language”: this is a mixture of SML (used mostly to write tactics) and HOL material (inside definitions and theorem statements). This script file is used to achieve inter-session persistence of the theory being constructed.

Once the user finishes the perhaps long and arduous task of constructing a theory, the user should

  1. check the script is separately compilable;
  2. invoke Holmake. This will (a) compile and execute the script file; and (b) compile the resulting theory file. After this, the theory file is available for use.

Source conventions for script and SML files

Script and theory files

The file that generates the HOL theory myTheory must be called myScript.sml. After the theory has been successfully generated, it can be open-ed at the head of other developments:

    open myTheory

and it can be loaded interactively:

    load "myTheory";

The file myScript.sml should begin with the standard boilerplate:

    open HolKernel Parse boolLib bossLib

    val _ = new_theory "my"

This “boilerplate” ensures that the standard tactics and SML commands will be in the namespace when the script file is compiled. Interactively, these modules have already been loaded and open-ed, so what can be typed directly at hol cannot necessarily be included as-is in a script file. In addition, if myTheory depends on other HOL theories, this ancestry should also be recorded in the script file. The easiest way to achieve this is simply to open the relevant theories. Conventionally, the open declarations for such theories appear just before the call to new_theory. For example:

    open HolKernel Parse boolLib bossLib

    open myfirstAncestorTheory OtherAncestorTheory

    val _ = new_theory "my"

Interactively, these may well be the names of theories that have been explicitly loaded into the context with the load function. In the interactive system, one has to explicitly load modules; on the other hand, the batch compiler will load modules automatically. For example, in order to execute open Foo (or refer to values in structure Foo) in the interactive system, one must first have executed load "Foo". (This is on the assumption that structure Foo is defined in a file Foo.sml.) Contrarily, the batch compiler will reject files having occurrences of load, since load is only defined for the interactive system.

In addition, simply referring to a theory's theorems using the ‘dot-notation’ will make that theory an ancestor. For example,

    Theorem mytheorem:
      ...
    Proof
      simp[ThirdAncestorTheory.important_lemma] ...
    QED

will record a dependency on ThirdAncestoryTheory, making it just as much an ancestor as the theories that have been explicitly open-ed elsewhere. This “trick” is not good practice however, and can lead to strange behaviours when working interactively. If it feels important to keep the top-level SML namespace uncontaminated, the right idiom for the top of the script file is

    local open ThirdAncestorTheory in end

Finally, all script files should also end with the invocation:

    val _ = export_theory()

When the script is finally executed, this call writes the theory to disk.

The calls to new_theory and export_theory must bracket a sequence of SML declarations and/or uses of special syntactic forms for scripts (the so-called “modern syntax”). The special syntactic forms all map to SML declarations, and include uses of keywords like Theorem, Type, and Overload. An SML declaration will typically be a val-binding, but might also be a function definition (via fun), an open, or even a structure declaration. Declarations are not expressions. This means that script files should not include bare calls to HOL functions like set_fixity. Instead, declarations such as the following need to be used:

    val _ = set_fixity "symbol" (Infixl 500);

This is because (due to restrictions imposed by Moscow ML) the script file is required to be an ML structure, and the contents of a structure must be declarations, not expressions. Indeed, one is allowed to (and generally should) omit the bracketing

    structure myScript = struct
    ...
    end

lines, but the contents of the file are still interpreted as if belonging to a structure.

Finally, take care not to have the string "Theory" appear at the end of the name of any of your files. HOL generates files containing this string, and when it cleans up after itself, it removes such files using a regular expression. This will also remove other files with names containing "Theory.sml" or "Theory.sig". For example, if, in your development directory, you had a file of SML code named MyTheory.sml and you were also managing a HOL development there with Holmake, then MyTheory.sml would get deleted if Holmake clean were invoked.

Other SML code

When developing HOL libraries, one should again attempt to follow Moscow ML's conventions. Most importantly, file names should match signature and structure names. If this can be done, the automatic dependency analysis done by Holmake will work “out of the box”. A signature for module foo should always appear in file foo.sig, and should have the form

    signature foo =
    sig
      ...
    end

The accompanying implementation of foo should appear in file foo.sml, and should have the form

    structure foo :> foo =
    struct
    ...
    end

As with theory files, the contents of a structure must be a sequence of declarations only. Neither sort of file should have any other declarations within it (before or after the signature or structure).

Deviations from this general pattern are possible, but life is much simpler if such deviations can be avoided. The HOL distribution1 contains some examples of trickier situations where the guidelines need to be ignored. Ignoring the guidelines will generally result in the need for quite involved Holmakefiles (see Section 10 below).

Summary

A complete theory construction might be performed by the following steps:

  • Construct theory script, perhaps over many sessions;
  • Transform script into separately compilable form;
  • Invoke Holmake to generate the theory and compile it.

After that, the theory is usable as an ML module. This flow is demonstrated in the Euclid example of TUTORIAL.

Alternatively, and probably with the help of one of the editor modes,2 one can develop a theory with a script file that is always separately compilable.

Multi-directory projects

Without information to the contrary, Holmake assumes that all files of interest are in the current directory or in HOL's master sigobj directory. Using a Holmakefile (see Section 10), it is possible to mention files in other directories (which can contain their own Holmakefiles), both as dependencies and as explicit targets with rules on how those targets should be built.

The right approach when a development spans multiple directories is to indicate that there is a dependency on other directories by using the INCLUDES variable in a Holmakefile.3 Holmake requires that, just as files must have an acyclic dependency graph, the directories containing those files must have a compatible, and acyclic, INCLUDES graph. With INCLUDES information to hand, Holmake will process the entire INCLUDES graph, looking throughout the graph for theory files that the current directory may depend on, and rebuilding those remote targets as necessary.

Cross-directory rules

Holmake reads every Holmakefile reachable from the starting directory through the INCLUDES graph and gathers their rules into a single shared rule database. Each rule retains an association with the Holmakefile that wrote it -- its owner -- and the owner's directory is where that rule's recipe will run. A target can pick up rule entries from more than one Holmakefile, and Holmake has to choose between them.

For any target T, two Holmakefiles in the loaded set are eligible to provide T's recipe:

  • the Holmakefile in T's own directory (if there is one), called T's home Holmakefile; and

  • the Holmakefile that named T as a prerequisite, called T's requestor -- the one whose work caused Holmake to need T in the first place.

A Holmakefile that is neither the home nor the requestor cannot supply a rule for T, even if it is in scope: the search space is restricted to those two by design, so that the recipe a target ends up with is always pinned to a Holmakefile the reader can point at directly.

In the common case T lives in the same directory as the Holmakefile that names it, so requestor and home coincide and there is only one rule to consider. When they differ, three configurations arise:

  • The requestor provides the rule; the home Holmakefile does not. Example: src/proofman/Holmakefile writes a rule for $(HOLDIR)/bin/hol.state0. The bin/ directory has no Holmakefile, so only src/proofman/Holmakefile has anything to say about that target. The recipe runs in src/proofman, which is why it can refer to proofManagerLib.uo (a file in src/proofman) without a path prefix.

  • The home Holmakefile provides the rule; the requestor does not. Example: Manual/Description/Holmakefile's mdbook target lists ../Tutorial/labels.tsv as a prerequisite, and the rule building labels.tsv lives in Manual/Tutorial/Holmakefile. Only Tutorial has the rule; the recipe runs in Manual/Tutorial.

  • Both provide a rule. Holmake refuses to pick one and aborts with a Conflicting rules error naming both Holmakefiles. There is no defined precedence; remove the duplicate.

If neither Holmakefile provides a rule and T exists on disk, T is treated as an external prerequisite (no rebuild attempted). If neither provides a rule and T does not exist, Holmake reports Don't know how to build necessary target(s).

Project files: holproject.toml

For a development that spans several sibling directories all owned by the same logical project, repeating INCLUDES lines in every sub-Holmakefile is tedious and error-prone. Dropping a holproject.toml file at the root of the development lets Holmake discover the project's directory layout automatically: every directory below the file (except dot-directories and entries in [exclude]) is treated as if it were implicitly available to every other directory in the project, whether or not it carries a Holmakefile.

The file is a small TOML document; the minimum is just a name:

name = "myproject"

With nothing else set, every directory below the file -- minus dot-dirs (.git, .hol, .claude, .svn) -- joins the project's project directory set. Source-only directories without a Holmakefile are full members; users do not need to scatter empty Holmakefiles to mark sub-directories. When Holmake is invoked from any directory inside the project (or at the root), it:

  • finds the project file by walking from cwd upward;
  • reads every project directory's Holmakefile (if any);
  • treats the project directory set as additional implicit INCLUDES -- so a Holmakefile in dirA no longer needs to write INCLUDES = ../dirB to reach dirB's targets or sources;
  • resolves cross-directory rule lookups and Holdep's source search across the whole set, the same way classical INCLUDES would.

Schema

The full schema (all keys optional except name):

name = "myproject"

# Opt out of project mode without giving up the file's other
# facilities.  Defaults to true; setting false suppresses the
# directory-set widening, cross-dir rule resolution, and source-
# name clash check, while leaving holpathdb registration (via
# `name`/`holpath`) and `external_includes` inheritance in place.
# See "Inheritance-only mode" below.
holmake = true

# Directories under the project root to keep out of the project
# directory set.  Paths are relative to the project root.  Useful
# for excluding scratch dirs, in-progress sub-projects, etc.
exclude = ["scratch", "doc/build"]

# Directories outside the project tree that every project dir
# treats as an implicit INCLUDES.  Most commonly these will be
# sibling examples/ subtrees; for projects linking to an already-
# built HOL, sources under src/ are reached via sigobj/ and need
# not be listed here.  $(HOLDIR) is substituted with the
# configure-time HOLDIR path; non-absolute paths are resolved
# relative to the project root.
external_includes = [
  "$(HOLDIR)/examples/foo",
  "../sibling-project",
]

# Other projects this one depends on.  The `path` is where to find
# that project on disk.  If the consumer's holproject.toml needs
# to override a subset of that other project's contents (for name
# clashes or to skip parts), an exclude list scopes to that
# external.
[projects.cakeml]
path = "/home/me/dev/cakeml"
exclude = ["scratch"]

A second file holproject.local.toml, intended to be gitignored, can sit alongside holproject.toml and carry the same [projects.<id>] tables. Entries with the same id in the local file override entries in the committed file, letting individual developers point externals at their own local paths.

[h4pedant]: style-check configuration

The optional [h4pedant] section configures the h4pedant style-check tool (tools/h4pedant). When h4pedant is invoked with no positional directory arguments, it walks up from the current directory to find a holproject.toml; if one is found it scans the project tree below it under whatever settings the [h4pedant] section declares. With no project file in the ancestor chain, h4pedant falls back to scanning the current directory. Positional directory arguments override the default choice of scan root but per-directory settings from the project file still apply.

[h4pedant]
linelen = 80           # max line length; 0 disables the check
unicode_ok = false     # if true, do not flag non-ASCII characters
exclude = ["scratch"]  # subtrees to skip entirely

[[h4pedant.dir]]
path = "src"
unicode_ok = true
linelen = 0

The [[h4pedant.dir]] array entries are per-subdirectory overrides keyed by path (relative to the project root); each setting that is present takes precedence over the global [h4pedant] default. Overrides apply recursively to everything below the named directory; where multiple overrides apply to a file, the deepest match wins key-by-key with shallower matches contributing any settings they declare and the deeper match leaves untouched.

CLI flags continue to take precedence over the project file: --nolinelen disables the line-length check everywhere even in directories whose override sets a positive linelen, and --unicodeok similarly suppresses the Unicode check globally. The project file can never re-enable a check that the CLI has turned off.

name and the path database

The name key doubles as the project's holpathdb registration: during startup Holmake walks the directory hierarchy for holproject.toml files and, for each file that sets a name, records that name as pointing at the file's directory. Other Holmakefiles (and dependency files) can then refer to the project by $(name)/relative/path and have the prefix expand to an absolute path at build time. Reverse lookup uses the registration to print paths portably in dependency lists.

name is otherwise optional: a holproject.toml that omits name still drives project mode (the directory-set widening described above), it just contributes no path-database entry.

Conflicting registrations are fatal: if two holproject.toml files reachable from the current build register the same name for different directories, or if a project's name collides with a built-in registration such as HOLDIR, Holmake reports the collision at startup and exits. The user resolves by renaming one of the projects.

Cross-directory build order

Holmake walks the project directories in classical post-order at the top of the build: every project directory's graph nodes are populated before the directory Holmake was invoked from has its own targets dispatched. Cross-directory dependency edges between project directories are recorded automatically when Holdep finds a reference (an open Foo for a structure that lives in another project dir, or Ancestors X in a theory script naming a theory that lives in another project dir).

The visiting order is alphabetical by absolute path. If project dir A's products depend on project dir B's products, the project's directory names must arrange B < A lexicographically (or the user must add an explicit INCLUDES = ../B line in A's Holmakefile, in which case classical traversal handles the order). In practice this is usually unsurprising and matches the naming the user already wants.

Source-name disambiguation

HOL has no per-project namespace separation: open Foo resolves to a Foo.uo on the search path, with no qualifier saying which directory it came from. So Holmake refuses to start if any two directories in the project's directory set contain a .sml or .sig source file with the same base name. The error names every offending file and points the user at the [exclude] key as the remedy:

Holmake: holproject.toml: ambiguous source name 'Foo.sml' reachable
from this build:
  /repo/src/A/Foo.sml
  /repo/src/B/Foo.sml
Resolve by listing one of the offending directories in the
[exclude] key of holproject.toml (or in [projects.<id>].exclude
for a directory inside an external project), or by renaming one
of the files.

The check covers .sml and .sig files, including theory scripts (e.g. duplicate BarScript.sml files in two project dirs would also fail -- the resulting BarTheory would be similarly ambiguous).

Consistency: INCLUDES vs. [exclude]

If a Holmakefile in a project dir explicitly writes INCLUDES = ../foo but the project file's [exclude] lists ../foo, Holmake aborts with a message naming both the contradictory Holmakefile and the [exclude] entry. The user must reconcile.

Inheritance-only mode: holmake = false

A holproject.toml containing holmake = false is parsed and its name/holpath and external_includes keys take effect as usual, but project mode is not activated: there is no directory-set widening, no cross-directory rule resolution, and no source-name clash check. The file behaves as a lightweight inheritance shim — useful when the top of a larger tree wants to share holpathdb registrations or external_includes with every Holmake invocation below, without forcing the whole tree into a single project.

Concretely, the canonical use is a shim at the root of a multi- project repository (the HOL repository itself is shipped with one): its presence stops find_root's upward walk, supplies any external_includes declared in it, and otherwise lets each sub-development behave classically.

Under holmake = false the project-mode-only keys exclude and [projects.<id>] are inert; Holmake warns at startup naming any that are present so the inconsistency can be removed.

This is distinct from the --no-project command-line flag, which ignores the file entirely (no external_includes, no holpathdb registration from it).

When project mode does not activate

Holmake decides once, at startup, whether project mode is on, based on whether a holproject.toml file lies on the chain of ancestor directories above cwd. Project mode does not fire when Holmake walks into a project's directory tree via classical INCLUDES from an aggregator like src/parallel_builds/core: the aggregator's invocation is from outside the project and there's no project file in its own ancestor chain. In that case the project's directories behave classically; their INCLUDES lines drive build order in the usual way.

The --no-project command-line flag suppresses project-mode detection regardless of holproject.toml's presence; useful for debugging or when a project's behaviour with and without project mode needs to be compared.

Holmake's command-line arguments

Like make, Holmake takes command-line arguments corresponding to the targets that the user desires to build. As a special case of this, theories and SML object files can be specified on the command-line by just giving the same string as would be passed to the open declaration form.4 Command-line targets do not have to provide paths to directories where those targets are “housed” if there is only target of the given name in the combination of all INCLUDE-d directories.

If there are no command-line targets, then Holmake will look for a Holmakefile in the current directory. If there is none, or if that file specifies no targets, then Holmake will attempt to build all SML modules and HOL theories it can detect in the current directory. If there is a target in the Holmakefile, then Holmake will try to build the first such target (only).

In addition, there are three special targets that can be used:

clean Removes all compiled files (unless over-ridden by a make-file target of the same name, see Section 10 below).

cleanDeps Removes all of the pre-computed dependency files. This can be an important thing to do if, for example, you have introduced a new .sig file on top of an existing .sml file.

cleanAll Removes all compiled files as well as all of the hidden dependency information.

Finally, users can directly affect the workings of Holmake with the following command-line options/flags:

-C <directory> or --directory=<directory> Change to the given directory before doing anything else, mimicking make's flag of the same name. When -C is given more than once, each subsequent invocation is interpreted relative to the previous, so -C foo -C bar is equivalent to -C foo/bar.

--cachekey <theory> Computes and prints a deterministic SHA1 hash (cache key) for the given theory target, then exits. The hash is based on the contents of the theory's dependencies: source files and ancestor .dat (theory data) files. Dependencies on .uo and .ui files are excluded; where a dependency on a Theory.uo or Theory.ui file exists, the corresponding .dat file is used instead. Dependencies are sorted by filename (with hash as a tiebreaker) to produce a canonical, machine-independent ordering. All dependencies must already be built; the command will fail if any dependency file does not exist. This option is intended for use in caching built theories in CI or similar workflows: if the cache key has not changed, the theory does not need to be rebuilt.

--dirs Treat the positional command-line arguments as root directories rather than build targets. Holmake will visit each directory in turn — semantically as if invoked separately in each — but fuses the work into a single dependency graph and runs everything under one parallel build scheduler. Each root contributes its own "must build" targets (the first target of its Holmakefile, falling back to the ordinary plausible-targets fallback when no Holmakefile is present). Each root's INCLUDES traversal starts with its own ancestor chain, so mutual references between sibling roots no longer trip the INCLUDES-loop detector — but a genuine cycle within one root's INCLUDES chain is still reported. Each directory reachable from any root is scanned and added to the unified graph exactly once. Clean targets (clean, cleanDeps, cleanAll) are rejected as positional arguments when --dirs is given; supply at least one directory.

-f <theory> Toggles whether or not a theory should be built in “fast” mode. Fast building causes tactic proofs (invocations of prove, store_thm, and the Theorem-Proof-QED form) to automatically succeed. This lack of soundness is marked by the fast_proof oracle tag. This tag will appear on all theorems proved in this way and all subsequent theorems that depend on such theorems. Holmake's default is not to build in fast mode.

--fast Makes Holmake's default be to build in fast mode (see above).

--force-lastmaker Overwrite any existing .hol/make-deps/lastmaker file that conflicts with the running Holmake's path, without prompting or aborting. See Section 10 below for the full picture.

--help or -h Prints out a useful option summary and exits.

--holdir <directory> Associate this build with the given HOL directory, rather than the one this version of Holmake was configured to use by default.

--holmakefile <file> Use the given file as a make-file. See Section 10 below for more on this.

-I <directory> Look in specified directory for additional object files, including other HOL theories. This option can be repeated, with multiple -I's to allow for multiple directories to be referenced. Files in directories specified in this way will be rebuilt if they are needed for the specified list of targets.

--interactive or -i Causes the HOL code that runs when a theory building file is executed to have the flag Globals.interactive set to true. This will alter the diagnostic output of a number of functions within the system.

-j<n> or --jobs=<n> Specify the maximum number of parallel jobs Holmake should use when building targets. Each job is a separate process, and so can only interfere with other jobs via their interactions with the file system. Under Moscow ML this option is ignored; its Holmake can only run jobs sequentially. If not set, the default value for this option is 4.

-k or --keep-going Causes Holmake to try to build all specified targets, rather than stopping as soon as one fails to build.

--logging Causes Holmake to record the times taken to build any theory files it encounters. The times are logged in a file in the current directory. The name of this file includes the time when Holmake completed, and when on a Unix system, the name of the machine where the job was run. If Holmake exits unsuccessfully, the filename is preceded by the string "bad-". Each line in the log-file is of the form theory-name time-taken, with the time recorded in seconds.

--no_holmakefile Do not use a make-file, even if a file called Holmakefile is present in the current directory.

--no_overlay Do not use an overlay file. All HOL builds require the presence of a special overlay file from the kernel when compiling scripts and libraries. This is not appropriate for compiling code that has no connection to HOL, so this option makes the compilation not use the overlay file. This option is also used in building the kernel before the overlay itself has been compiled.

--no_preexecs Do not search for or execute any .hol_preexec files in the file-system. See Section 10 below for more on this facility.

--no_prereqs Do not recursively attempt to build “include” directories before working in the current directory. If a target in the current directory depends on something in another directory that does not exist, Holmake will fail to build it. If the remote target exists, but is stale, it will be used in its stale state, come what may.

--no_sigobj Do not link against HOL system's directory of HOL system files. Use of this option goes some way towards turning Holmake into a general SML make system. However, it will still attempt to do “HOL things” with files whose names end in Script and Theory. This option implies --no_overlay.

--overlay <file> Use the given file as the overlay rather than the default.

--qof, --noqof Where q-o-f stands for “quit on failure”. By default, if a tactic fails to prove a theorem, the running script exits with a failure. Depending on the presence or absence of the -k flag, this failure to build a theory may cause Holmake to also exit (with a failure). With the --noqof option, Holmake will cause the running script to use mk_thm to assert the failed goal, allowing the build to continue and other theorems to be proved. Either way, the running script also writes a Poly/ML heap snapshot so that the failing proof can be inspected interactively — see Section 10 below.

--quiet Minimise the amount of output produced by Holmake. Fatal error messages will still be written to the standard error stream. Note that other programs called by Holmake will not be affected.

-r Forces Holmake to behave more recursively than it would otherwise. This overrides the --no_prereqs option. When performing a “clean” action (prompted by clean, cleanAll or cleanDeps arguments), this cleaning is done recursively through all “includes” directories (which is not done otherwise). When building normally, all targets in “includes” directories are built; normally only dependencies of targets in the current directory are built.

--rebuild_deps Forces Holmake to always rebuild the dependency information for files it examines, whether or not it thinks it needs to. This option is implemented by having Holmake wipe all of its dependency cache (as per the cleanDeps option above) before proceeding with the build.

Holmake should never exit with error messages such as “Uncaught exception”. Such behaviour is a bug, please report it!

Multiple HOL installations and the lastmaker file

When a machine has more than one HOL installation, .hol/make-deps/lastmaker records the absolute path of the Holmake binary that most recently processed each directory. The file is a single line of text and has two consumers.

Holmake itself consults lastmaker at the start of every run. If the working directory is not under any HOL installation's tree5 and a lastmaker file already exists, Holmake exec-switches to the binary named there, so a directory previously built by another HOL installation is silently re-processed by that installation's Holmake. The pre-existing --nolmbc flag suppresses both this exec-switch behaviour and the propagation writes described below; it is useful when one deliberately wants to run this Holmake over a tree built by a different installation.

The second consumer is the editor modes (notably the emacs and vim modes), which read lastmaker to decide which hol binary to launch for an interactive session in that directory.

A lastmaker file is written in every directory Holmake visits during its INCLUDES walk, not just the directory Holmake was started in. This way, opening a script in an INCLUDES'd subtree in an editor still picks the right binary. The propagation is suppressed (no lastmaker files are written) when the invocation itself was launched from inside any HOL installation: the directory hierarchy already disambiguates, and INCLUDES walks rooted inside an installation do not leave it.

Conflicting lastmaker files

When propagation would overwrite an existing lastmaker that points at a different but still-usable Holmake binary, the user is asked what to do. The default ("N") aborts the build without modifying the existing file, preserving whatever state the other HOL installation left behind; answering "y" overwrites and proceeds, accepting that the current Holmake will treat the existing build artefacts as stale and likely rebuild them.

When standard input is not connected to a terminal — a continuous-integration run, a recursively-spawned child Holmake, an editor probe — there is no way for the user to consent, so Holmake aborts with a non-zero exit code and leaves the existing lastmaker file alone. The --force-lastmaker command-line flag suppresses the prompt and forces the overwrite in both interactive and non-interactive contexts; this is useful in batch contexts where the conflict is known and intentional.

A lastmaker whose recorded path no longer resolves to a real executable is treated as garbage and replaced silently, without any prompt.

Heap dumps on tactic failure

When a tactic fails inside store_thm, Q.store_thm, or the Theorem-Proof-QED form during a non-interactive Holmake build, the running script's Poly/ML heap is saved to a file named

   <theory>.<thmname>.dumpedheap

in the script's working directory. Before the heap is written, the proof manager is seeded with the failing goal so that, on reload, the failing proof is sitting on the goal stack ready to be explored. Resume with

   bin/hol --holstate=<theory>.<thmname>.dumpedheap

and the standard proof-manager commands (e, b, p, ...) work as usual against the original goal.

The dump is produced in both --qof and --noqof modes:

  • Under --qof (the default), the heap is saved and Holmake then exits with failure on the first failing proof.

  • Under --noqof, the heap is saved, a CHEAT-tagged oracle theorem is substituted for the failing proof, and the build continues. A script that fails on multiple proofs will therefore leave multiple .dumpedheap files behind.

Under --fast the tactic is never evaluated, so no dump is produced. The mechanism is also a no-op under Moscow ML, which has no equivalent of Poly/ML's SaveState.saveChild.

When Holmake next rebuilds a theory script, any <theory>.*.dumpedheap file in the script's directory is swept along with the previous run's Theory.sml/.sig/.dat outputs, so stale dumps do not accumulate across iterations. Dumps produced by bin/hol run outside the build system are left in place and can be deleted freely once the failing proof has been resolved.

Using a make-file with Holmake

Holmake will use a make-file to augment its behaviour if one is present in the current directory. By default it will look for a file called Holmakefile, but it can be made to look at any file at all with the --holmakefile command-line option. The combination of Holmake and a make-file is supposed to behave as much as possible like a standard implementation of make.

A make-file consists of three kinds of entries: variable definitions, rules, and top-level function-call expressions. A top-level function-call expression is a line whose first non-whitespace character is $ (e.g. $(info hello)); the line is expanded for its side effects and its result is discarded (reported as an error if it is non-empty). Outside of these entries, white-space is insignificant, but newline and TAB characters are very significant within them. Comments can be started with hash (#) characters and last until the end of the line. Quoting is generally done with use of the back-slash (\) character. In particular, a backslash-newline pair always allows a line to be continued as if the newline wasn't present at all.

A variable definition is of the form $$\textsl{Ident} \;\;\textsl{op}\;\; \textsl{text}\;\; \langle\texttt{NEWLINE}\rangle$$ where $\textsl{op}$ is either = or +=.

A rule is of the form $$\textsl{text } \texttt{:} \textsl{ text} \;\; \langle\texttt{NEWLINE}\rangle \;\; (\langle\texttt{TAB}\rangle\textsl{text} \;\; \langle\texttt{NEWLINE}\rangle)^*$$ Henceforth, the text following a TAB character in a rule will be referred to as the command text. Text elsewhere will be referred to as normal text. Normal text has comments stripped from it, so hash characters there must be escaped with a back-slash character. An Ident is any non-empty sequence of alpha-numeric characters, including the underscore (_).

In some contexts, normal text is interpreted as a list of words. These lists use white-space as element separators. If a word needs to include white-space itself, those white-space characters should be escaped with back-slashes.

Variable definitions and references

The text on the RHS of a variable definition can be substituted into any other context by using a variable reference, of the form $(VARNAME). References on right-hand sides are evaluated late, at time of use, not at time of definition. This means it is permissible to have forward references. This makes it impossible to write things like $$\texttt{VAR = \$(VAR) something\_new}$$ because the eventual evaluation of $(VAR) would lead to an infinite loop. GNU make's facility for immediate definition of variables with := is not supported, but the += definition form does allow variables to be “extended”. In particular, this form “acts essentially as if you had included the extra text in the initial definition of the variable” (as per GNU make's documentation). Extending a variable VAR in this way does not constitute a use of this variable, so any variable references in its original definition will remain unexpanded until VAR is used in a rule.

Note also that white-space around the equals-sign in a variable definition is stripped. This means that $$\texttt{VAR =}\langle\texttt{whitespace}\rangle\langle\texttt{NEWLINE}\rangle$$ gives VAR the empty string as its value.6

Finally, note that the text inside a variable reference is itself evaluated. This means that one can write something like $(FOO_$(OS)) and have this first expand the OS variable, presumably giving rise to some useful string (such as unix), and then have the resulting variable (FOO_unix, say) expanded. This effectively allows the construction of functions by cases (define variables FOO_unix, FOO_macos etc.; then use the nested variable reference above). If the internal variable expands to something containing spaces, this will not turn a normal variable reference into a function call (see below). On the other hand, if the initial reference contains a space, the function name component will be expanded, allowing implementation of a function by cases determining which text-manipulation function should be called.

Rules

Make-file rules are interpreted in the same way as by traditional make. The files specified after the colon (if any) are those files that each target (the files before the colon) is said to “depend” on. If any of these are newer than a target, then Holmake rebuilds that target according to the commands. If there are no dependencies, then the commands are executed iff the target doesn't exist. If there are no commands, and the target is not of a type that Holmake already knows how to build, then it will just make sure that the dependencies are up to date (this may or may not create the target). If there are no commands attached to a rule, and the target is one that Holmake does know how to build, then the rule's extra dependencies are added to those that Holmake has managed to infer for itself, and Holmake will build the target using its built-in rule. If commands are provided for a type of file that Holmake knows how to build itself, then the make-file's commands and dependencies take precedence, and only they will be executed.

In addition, it is possible to indicate that the built-in process of generating theory files from script files generates side products. This is done by writing a command-less rule of the form

   target : *thyScript.sml

where an asterisk character precedes the name of the script file. This indicates that the action of executing the code in thyScript.sml will not only generate the usual thyTheory.sig and thyTheory.sml files, but also the file target. If Holmake is asked to build any of these three files, and any is absent or out of date with respect to thyScript.sml (or any other dependency), then the code in thyScript.sml will be run.

If a command-line is preceded by a hyphen (-) character, then the rest of the line is executed, but its error-code is ignored. (Normally, a command-line raising an error will cause Holmake to conclude that the target can not be built.) If a command-line is preceded by an at-sign (@), then that command-line will not be echoed to the screen when it is run. These two options can be combined in either order at the start of a command-line.

Command text is interpreted only minimally by Holmake. On Unix, back-slashes are not interpreted at all. On Windows, back-slashes followed by newlines are turned into spaces. Otherwise, command text is passed as is to the underlying command interpreter (/bin/sh say, on Unix, or COMMAND.COM on Windows). In particular, this means that hash-characters do not start comments on command-lines, and such “comments” will be passed to the shell, which may or may not treat them as comments when it sees them.

Pattern rules

A rule whose target(s) contain the percent (%) character is a pattern rule. Pattern rules supply a single recipe for an open-ended family of targets, with the % acting as a wildcard ("stem") that is substituted into the recipe and into the rule's prerequisites. The shape of a percent-pattern is the same as that used by $(patsubst ...) (see below): one % per pattern, matching any non-empty string; a literal % is written as \%.

The simplest form of pattern rule is

%.ext: %.other
        recipe using $<, $@, etc.

When Holmake is asked to build a target whose name fits %.ext with some stem S, it looks for a file named S.other (or a rule that builds it) and runs the recipe. Multiple targets in a single rule are permitted and share both the prerequisites and the recipe; each target is matched against the percent-pattern independently:

%.uo %.ui: %.sml common.ui
        $(MOSMLC) -c $<

Within a pattern rule's recipe the following automatic variables are bound:

  • $@: the target being built (foo.uo in the example above).
  • $<: the first matched prerequisite (foo.sml).
  • $*: the stem the percent matched (foo).
  • $^: the (space-joined) full list of prerequisites (foo.sml common.ui).

A pattern rule can be combined with an ordinary command-less rule to give a particular target additional prerequisites without disturbing the recipe. For instance, building on the pattern rule above, writing

specific.uo: extra.ui

adds extra.ui as a prerequisite of specific.uo; the recipe still comes from the %.uo %.ui: %.sml common.ui pattern, and $^ expands to include the extra dependency.

A pattern rule is only applicable to a target when each of the target's substituted prerequisites either already exists on disk or is itself the target of some explicit rule. This is the same two-phase implicit-rule search performed by GNU make; without it, a rule like %.tex: %.stex would also claim hand-maintained .tex files in the same directory whose .stex source does not exist.

The precedence rules between pattern rules and the other kinds of rule are as follows. For a target T, Holmake consults, in order:

  1. an explicit rule for T that carries a recipe;
  2. Holmake's built-in productions for source files (Script.sml to Theory.{sml,sig,uo}, .sml to .uo, .sig to .ui), when applicable;
  3. pattern rules in the order they appear in the Holmakefile;
  4. the existing-file/no-rule fallback.

In particular, the built-in productions take precedence over any user-supplied pattern rule that would otherwise match a target of those shapes. A user who needs to override a built-in production can still do so with an explicit recipe-carrying rule for the specific target.

Example. Suppose a directory holds several .stex chapter sources that are all processed into .tex by a script called polyscripter. Without pattern rules, each chapter needs its own rule:

euclid.tex: euclid.stex ../Tools/polyscripter
        ../Tools/polyscripter < $< > $@

parity.tex: parity.stex ../Tools/polyscripter
        ../Tools/polyscripter < $< > $@

# ... and so on for each chapter

A single pattern rule covers all of them, with chapter-specific extra prerequisites listed separately:

%.tex: %.stex ../Tools/polyscripter
        ../Tools/polyscripter < $< > $@

euclid.tex: euclid-extras.ML
proof-tools.tex: ../../examples/dpll.sml

Because of the applicability rule above, hand-maintained .tex files such as tutorial.tex (which have no corresponding .stex) are not affected by this pattern: Holmake sees that tutorial.stex neither exists nor has a rule, rejects the pattern, and falls through to treating tutorial.tex as an ordinary leaf file.

Pattern targets in subdirectories. A pattern target may carry a literal directory prefix, in which case it matches targets in that subdirectory of the Holmakefile's own directory. For instance, the rule

figs/%.svg: figs/%.mps
        cd figs && mptopdf $*.mps
        pdftocairo -svg figs/$*-mps.pdf $@

claims every figs/*.svg whose matching figs/*.mps source exists (and only those — the applicability rule still applies), and would collapse a dozen explicit per-figure rules into one. The path is interpreted relative to the directory holding the Holmakefile; patterns can reach into subdirectories but not across into siblings or upward into parents.

A pattern with no literal directory prefix also fires for targets in subdirectories: the stem then absorbs the directory separator. Writing %.svg: %.mps would match figs/0.svg with stem figs/0, looking for figs/0.mps as the prerequisite. Use whichever form expresses the intent more clearly: an explicit prefix is usually clearer when the rule is scoped to a specific subdirectory, while the bare form is handy for "convert every .x to .y, wherever it lives" recipes.

Special targets

Some target names for rules are handled specially by Holmake:

  • Dependencies associated with the target name .PHONY are taken to be list of other targets in the make-file that are not actually the name of files to be built. For example, targets naming conceptual collections of files such as all should be marked as “phony”. If a target is phony, then its dependencies will be built even if a file of that name exists and is newer than the dependencies.
  • The special way that command-line arguments clean, cleanAll and cleanDeps are handled means that targets of those names will not work. In order to extend cleaning behaviour, use the EXTRA_CLEANS variable (see below).

Functions

Holmake supports some simple functions for manipulating text. All functions are written with the general form $(function-name arg${}_1$,arg${}_2$...,arg${}_n$). Arguments to most functions cannot include commas: use a variable reference to a variable whose value is a comma instead. The exceptions are $(info), $(warning) and $(error), which rejoin their arguments with literal commas before emitting them. Otherwise, arguments can be arbitrary text.

$(dprot arg) quotes (or “protects”) the space characters that occur in a string so that the string will be treated as a unit if it occurs in a rule's dependency list. For example, the file

   dep = foo bar
   target: $(dep)
      do_something

will see target as having two dependencies, not one, because spaces are used to delimit dependencies. If a dependency's name includes spaces, then this function can be used to quote them for Holmake's benefit. Note that the dprot function does not do the same thing as protect on either Unix or Windows systems.

$(error msg) (a GNU make compatibility function) writes <file>:<line>: *** <msg>. Stop. to standard error and aborts Holmake with a non-zero exit status. The reported location is the use site — i.e. for X = $(error oops), the message points at the line that references $(X), not the line that defined X. (GNU make reports the definition site instead.)

$(findstring arg1,arg2) checks if arg1 occurs in (is a sub-string of) arg2. If it does so occur, the result is arg1, otherwise the result is the empty string.

$(if arg1,arg2,arg3) examines arg1. If it is the empty string, then the value of the whole is equal to the value of arg3. Otherwise, the value is that of arg2.

$(info msg) (a GNU make compatibility function) writes msg followed by a newline to standard output. The expansion of the call itself is the empty string. Unlike $(warning) and $(error), no <file>:<line>: prefix is added.

$(patsubst arg1,arg2,text) splits text into component words, and then transforms each word by attempting to see if it matches the pattern in arg1. If so, it replaces that word with arg2 (suitably instantiated). If not, the word is left alone. The modified words are then reassembled into a white-space separated list and returned as the value.

A pattern is any piece of text including no more than one occurrence of the percent (%) character. The percent character matches any non-empty string. All other characters must be matched literally. The instantiation for % is remembered when the replacement is constructed. Thus, $$\texttt{\$(patsubst \%.sml,\%.uo,\$(SMLFILES))}$$ turns a list of files with suffixes .sml into the same list with the suffixes replaced with .uo.

$(protect arg) wraps arg in appropriate quote characters to ensure that it will pass through the operating system's command shell unscathed. This is important in the presence of file-names that include spaces or other shell-significant characters like less-than and greater-than. Those make-file variables that point directly at executables (MOSMLC, MOSMLLEX etc.) are automatically protected in this way. Others, which might be used in concatenation with other elements, are not so protected. Thus, if DIR might include spaces, one should write

   $(protect $(DIR)/subdirectory/program)

so that the above will be read as one unit by the underlying shell.

$(subst arg1,arg2,text) replaces every occurrence of arg1 in text with arg2.

$(tee arg1,arg2) produces a (moderately complicated) shell command line that behaves like arg1 | tee arg2, but whose exit code is arg1's rather than tee's.

$(warning msg) (a GNU make compatibility function) writes <file>:<line>: <msg> followed by a newline to standard error. The expansion of the call itself is the empty string. As with $(error), the location reported is the use site rather than the definition site.

$(which arg) is replaced by the full path to an executable and readable occurrence of a file called arg within a directory in the list of directories in the PATH environment variable. For example $(which cat) will usually expand to /bin/cat on Unix-like systems. If there is no occurrence of arg in any directory in PATH, this function call expands to the empty string.

$(wildcard pattern) expands the shell “glob” pattern (e.g., *Script.sml) into the list of matching filenames. If the pattern doesn't match any files, then the function returns pattern unchanged.

Special and pre-defined variables

If defined, the INCLUDES variable is used to add directories to the list of directories consulted when files are compiled and linked. The effect is as if the directories specified had all been included on the command-line with -I options. The INCLUDES directories are consulted before the distribution's sigobj directory (containing all core material).

By default, directories specified in the INCLUDES list are also built by Holmake before it attempts to build in the current directory. If the -r (“force recursion”) command-line flag is used, these directories are also “clean”-ed when a cleaning target is given to Holmake.

The CLINE_OPTIONS variable is used for the specification of command-line switches that are presumably usually appropriate for calls to Holmake in the containing directory. The options present in CLINE_OPTIONS are used to build a “base environment” of switches; this base environment is then overridden by whatever was actually passed on the command-line. For example, a useful CLINE_OPTIONS line7 might be

    CLINE_OPTIONS = -j1 --noqof

Under Poly/ML, the similar POLY_CLINE_OPTIONS variable can be used to pass run-time options to the Poly/ML executable that is run during theory construction.

The EXTRA_CLEANS variable is used to specify the name of additional files that should be deleted when a Holmake clean command is issued.

Within a command, the variable $< is used to stand for the name of the first dependency of the rule. The variable $@ is used to stand for the target of the rule.

Finally there are variables that expand to program names and other useful information:

CP This variable is replaced by an operating-system appropriate program to perform a file copy. The file to be copied is the first argument, the second is the place to copy to. The second argument can be a directory. (Under Unix, CP expands to /bin/cp; under Windows, it expands to copy.)

DEBUG_FLAG This variable is replaced by "--dbg" if that flag was passed to Holmake, or the empty string if not.

DEFAULT_TARGETS This variable expands to a list of the targets in the current directory that Holmake would build if there was no target in the Holmakefile, and no target was specified on the command-line. Thus, if one wishes to continue to have all these defaults built alongside an additional target, an appropriate idiom to use at the head of the file would be

    all: $(DEFAULT_TARGETS) mytarget1 mytarget2
    .PHONY: all

followed by rules for building the new target(s).

HAVE_WORD64 Set (to value "1") if the SML implementation provides a Word64 structure, and undefined otherwise.

HOLDIR The root of the HOL installation.

HOLHEAP Under Poly/ML, this variable expands to the name of the heap that should be used to build this directory (to be used instead of the heap that underlies the hol executable). See Section 10.1 below for more on using custom heaps with Poly/ML.

HOLMOSMLC This variable is replaced by an invocation of the Moscow ML compiler along with the -q flag (necessary for handling quotations), and the usual -I include specifications (pre-includes, the hol-directory include, and the normal includes).

HOLMOSMLC-C This variable is the same as HOLMOSMLC except that it finishes with a closing -c option (hence the name) followed by the name of the system's overlay file. This is needed for compilation of HOL source files, but not for linking of HOL object code, which can be done with HOLMOSMLC.

HOL_NUMJOBS The value of the -j option (4 by default under Poly/ML, 1 under Moscow ML) controlling the number of parallel jobs that Holmake will use.

KERNELID The kernel option that was passed to HOL's build command, stripped of its leading hyphens. This will typically be stdknl (the standard kernel) but may take on other values if other custom kernels are being used.

LOCAL_PARALLELISM_LIMIT Under Poly/ML, setting this variable to a positive integer n instructs the parallel scheduler that no target in this directory may be dispatched unless the total number of jobs that would then be running across the whole build is at most n. In particular, LOCAL_PARALLELISM_LIMIT = 1 reserves exclusive use of the machine for any target in this directory (useful for theory builds whose memory footprint would otherwise provoke OOMs when run alongside other jobs under a large -j N). A right-hand side that is not a single positive integer is reported with a warning and ignored. The variable has no effect under Moscow ML or under Poly/ML Holmake -j 1.

ML_SYSNAME The name of the ML system being used: either mosml or poly.

MLLEX This is the path of the mllex tool that is built as part of HOL's configuration.

MLYACC This is the path of the mlyacc tool that is built as part of HOL's configuration.

MOSMLC This is replaced by an invocation of the compiler along with just the normal includes.

MOSMLLEX This is replaced by an invocation of the mosmllex program that comes with the Moscow ML distribution.

MOSMLYAC This is replaced by an invocation of the mosmlyac program that comes with the Moscow ML distribution.

MV This variable is replaced by an operating-system appropriate program to perform a file movement. The file to be moved is the first argument, the second is the place to move to. The second argument can be a directory. (Under Unix, MV expands to mv; under Windows, it expands to rename.)

OS This variable is replaced by the name of the current operating system, which will be one of the strings "linux", "solaris", "macosx", "unix" (for all other Unices), or "winNT", for all Microsoft Windows operating systems (those of the 21st century, anyway).

SIGOBJ Effectively $(HOLDIR)/sigobj, where HOL object code is stored.

UNQUOTE The location of the quotation-filter executable.

The MOSMLLEX and MOSMLYAC abbreviations are really only useful if the originals aren't necessarily going to be on the user's “path”. For backwards compatibility, the five variables above including the sub-string "MOSML" in their names can also be used by simply writing their names directly (i.e., without the enclosing $(...)), as long as these references occur first on a command-line.

Under Poly/ML, commands involving the variable MOSMLC are interpreted “appropriately”. If the behaviour is not as desired, we recommend using ifdef POLY (see below) to write rules that pertain only to HOL under Poly/ML. We strongly discourage the use of MOSMLYAC and MOSMLLEX, even when running HOL under Moscow ML.

If a reference is made to an otherwise undefined string, then it is treated as a reference to an environment variable. If there is no such variable in the environment, then the variable is silently given the empty string as its value.

Conditional parts of makefiles

As in GNU make, parts of a Holmakefile can be included or excluded dynamically, depending on tests that can be performed on strings including variables. This is similar to the way directives such as #ifdef can be used to control the C preprocessor.

There are four possible directives in a Holmakefile: ifdef, ifndef, ifeq and ifneq. The versions including the extra ‘n’ character reverse the boolean sense of the test. Conditional directives can be chained together with else directives, and must be terminated by the endif command.

The following example is a file that only has any content if the POLY variable is defined, which happens when Poly/ML is the underlying SML system.

ifdef POLY
TARGETS = target1 target2

target1: dependency1
      build_command -o target1 dependency1
endif

The next example includes chained else commands:

ifeq "$(HOLDIR)" "foo"
VAR = X
else ifneq "$(HOLDIR)" "bar"
VAR = Y
else
VAR = Z
endif

The ifneq and ifeq forms test for string equality. They can be passed their arguments as in the example, or delimited with apostrophes, or in parentheses with no delimiters, as in:

ifeq ($(HOLDIR),$(OTHERDIR))
VAR = value
endif

The definedness tests ifdef and ifndef test if a name has a non-null expansion in the current environment. This test is just of one level of expansion. In the following example, VAR is defined even though it ultimately expands to the empty string, but NULL is not. The variable FOOBAR is also not defined.

NULL =
VAR = $(NULL)

Note that environment variables with non-empty values are also considered to be defined.

Including other makefiles

As in GNU make, a Holmakefile can pull in another file's contents inline. Three directives are supported:

include   FILE1 FILE2 ...
-include  FILE1 FILE2 ...
sinclude  FILE1 FILE2 ...

The include form is mandatory: if any named file is missing, Holmake reports an error and stops. The -include and sinclude forms are synonyms for the missing-file-tolerant variant — missing files are silently skipped.

Filenames are whitespace-separated; multiple files on one directive line are read in order. $(VAR) references in the filename are expanded against the variables defined so far in the including file, matching GNU make's immediate-expansion timing, so

SUBDIR = config
include $(SUBDIR)/local.mk

reads config/local.mk. Relative paths are resolved against the directory of the including file, not the current working directory, so a Holmakefile in foo/bar/ saying include ../shared.mk picks up foo/shared.mk regardless of where Holmake was invoked from.

Includes nest: an included file may itself contain further include directives. Cycles (A includes B includes A) are detected and reported as an error. Conditional directives must balance within each file — an ifdef opened in an included file must be closed in the same file (just as GNU make requires) — but it is fine to wrap an include directive in a conditional in the including file.

A typical use is to factor a list of common variables out of a group of sibling Holmakefiles into a single shared snippet:

# shared.mk
COMMON_FLAGS = -I ../foo -I ../bar
SOURCES = main.sml util.sml
# subdir/Holmakefile
include ../shared.mk
target.uo: $(SOURCES)
   $(HOLMOSMLC) $(COMMON_FLAGS) -c -o $@ $(SOURCES)

Execution of Commands before Holmake Begins

When building complicated projects, it may be useful to have programs executed before any invocation of Holmake properly begins. This can be done through the use of .hol_preexec files. When Holmake begins, and before it has even begun to examine Holmakefiles, it scans upwards in the directory hierarchy looking for files of this name. This scanning will also follow INCLUDES directives, possibly causing a jump sideways in the hierarchy.

When this scanning is complete, the contents of all these files will be executed as shell commands (using the OS.Process.system command, which is in turn a wrapper for the standard C library system function). Each command will be interpreted in the directory containing it, and (under Unix at least), in an environment where the HOLORIG variable is set to the path of the directory where Holmake was originally invoked. The commands are executed sequentially in a pre-order depth-first traversal of the directory hierarchy (relying on the behaviour of String.compare on paths). If any command fails (returns a non-zero exit code), Holmake will abort; in this way pre-execution commands can be used as checks as well as commands that are supposed to bring about useful changes to the state of the file-system.

Warning: This facility allows for code at a higher-level (in terms of the parent-child relationship between paths) in the file-system hierarchy to execute. Holmake will log all such executions by default, printing out the commands as it executes them. This execution does not happen with “clean” targets, or if the -h or --no_preexecs command-line options are used. It is not possible to stop this behaviour by putting the --no_preexecs option into a CLINE_OPTIONS variable with a Holmakefile; all such execution will have occurred before any such are really consulted.

Generating and Using Heaps in Poly/ML HOL

Poly/ML has a nice facility whereby the state of one of its interactive sessions can be stored on disk and then reloaded. This allows for an efficient resumption of work in a known state. The HOL implementation uses this facility to implement the hol executable. In Poly/ML, hol starts immediately. In Moscow ML, hol starts up by visibly (and relatively slowly) “loading” the various source files that provide the system's functionality (e.g., bossLib).

Users can use the same basic technology to “dump” heaps of their own. Such heaps can be preloaded with source code implementing special-purpose reasoning facilities, and with various necessary background theories. This can make developing big mechanisations considerably more pleasant.

Generating HOL heaps

The easiest way to generate a HOL heap is to use the hol buildheap subcommand that is part of the standard hol executable for (Poly/ML) HOL. This subcommand takes a list of object files to include in a heap, an optional heap to build upon (use the -b command-line switch; the default is to use the heap behind the core hol executable), and a required name for the new heap (the -o switch). Thus the command-line

   hol buildheap -o realheap transcTheory polyTheory

would build a heap in the current directory called realheap, and would preload it with the standard theories of transcendental numbers and real-valued polynomials.

A reasonable way to manage the generation of heaps is to use a Holmakefile. For example, the realheap above might be generated with the source in Figure 10.1.1. The use of the special variable HOLHEAP has a number of nice side effects. First, it makes the given file a dependency of all other products in the current directory. This means that the HOL heap will be built first. Secondly, the other products in the current directory will be built on top of that heap, not the default heap behind hol.

Figure: A Holmakefile fragment for building a custom HOL heap embodying the standard real number theories.

ifdef POLY
HOLHEAP = realheap
OBJNAMES = polyTheory transcTheory
DEPS = $(patsubst %,$(dprot $(SIGOBJ)/%),$(OBJNAMES))

$(HOLHEAP): $(DEPS)
        $(protect $(HOLDIR)/bin/hol) buildheap -o $@ $(OBJNAMES)
endif

If the heap's dependencies are not core HOL theories as they are here, then both the dependency line and the arguments to hol buildheap will need to be adjusted to link to the directory containing the files. For core HOL theories, the dependency has to mention the SIGOBJ directory, but when passing arguments to hol buildheap, that information doesn't need to be provided as SIGOBJ is always consulted by all HOL builds. Finally, note how the use of the dprot and protect functions will ensure that Holmake will do the right thing even when HOLDIR contains spaces.

Using HOL heaps

As just described, if a Holmakefile specifies a HOLHEAP, then files in that directory will be built on top of that heap rather than the default. This is also true if the specified heap is in another directory (i.e., the HOLHEAP line might specify a file such as otherdir/myheap). In this case, the Holmakefile won't (shouldn't) include instructions on how to build that heap, but the advantages of that heap are still available. Again, that heap is also considered a dependency for all files in the current directory, so that they will be rebuilt if it is newer than they are.

It is obviously important to be able to use heaps interactively. If the standard hol executable is invoked in a directory where there is a Holmakefile specifying a heap, the default heap will not be used and the given heap will be used instead. The fact that this is happening is mentioned as the interactive session begins. For example:

---------------------------------------------------------------------
       HOL-4 [Kananaskis 8 (stdknl, built Tue Jul 24 16:48:44 2012)]

       For introductory HOL help, type: help "hol";
---------------------------------------------------------------------

[extending loadPath with Holmakefile INCLUDES variable]
[In non-standard heap: computability-heap]
Poly/ML 5.4.1 Release
>

Finally, note that when using the HOLHEAP variable, heaps are required to be built before everything else in a directory, and that such heaps embody theories or SML sources that are ancestral to the directory in which the heap occurs. Thus, if one wanted to package up a heap embodying the standard theories for the real numbers, and to do it in src/real (which feels natural), this heap could be built using the method described here, but could only be referred to as a HOLHEAP in the directories that used it, not in src/real's Holmakefile. Subsequently, developments in other directories could use this heap by specifying

   $(HOLDIR)/src/real/realheap

as the value for their HOLHEAP variables.

Timing and Counting Theorems

HOL can be made to record its use of primitive inferences, axioms, definitions and use of oracles. Such recording is enabled with the function

   val counting_thms : bool -> unit

(This function as with all the others in this section is found in the Count structure.)

Calling counting_thms true enables counting, and counting_thms false disables it. The default is for counting to be disabled. If it is enabled, whenever HOL performs a primitive inference (or accepts an axiom or definition) a counter is incremented. A total count as well as counts per primitive inference are maintained. The value of this counter is returned by the function:

  val thm_count : unit ->
   {ASSUME : int, REFL : int, BETA_CONV : int, SUBST : int,
    ABS : int, DISCH : int, MP : int, INST_TYPE : int, MK_COMB : int,
    AP_TERM : int, AP_THM : int, ALPHA : int, ETA_CONV : int,
    SYM : int, TRANS : int, EQ_MP : int, EQ_IMP_RULE : int,
    INST : int, SPEC : int, GEN : int, EXISTS : int, CHOOSE : int,
    CONJ : int, CONJUNCT1 : int, CONJUNCT2 : int, DISJ1 : int,
    DISJ2 : int, DISJ_CASES : int, NOT_INTRO : int, NOT_ELIM : int,
    CCONTR : int, GEN_ABS : int, definition : int, axiom : int,
    from_disk : int, oracle :int, total :int }

This counter can be reset with the function:

   val reset_thm_count : unit -> unit

Finally, the Count structure also includes another function which easily enables the number of inferences performed by an SML procedure to be assessed:

   val apply : ('a -> 'b) -> 'a -> 'b

An invocation, Count.apply f x, applies the function f to the argument x and performs a count of inferences during this time. This function also records the total time taken in the execution of the application.

For example, timing the action of numLib's ARITH_CONV:

- Count.apply numLib.ARITH_CONV ``x > y ==> 2 * x > y``;
runtime: 0.010s,    gctime: 0.000s,     systime: 0.000s.
Axioms asserted: 0.
Definitions made: 0.
Oracle invocations: 0.
Theorems loaded from disk: 0.
HOL primitive inference steps: 165.
Total: 165.
> val it = |- x > y ==> 2 * x > y = T : thm

  1. See, for example, the kernel implementation in src/0.

  2. There are editor modes for emacs, vim, and others.

  3. In one-off situations, it is also possible to use the -I flag on Holmake's command-line.

  4. Strictly, the files generated on disk for these cases have a .uo suffix; this feature allows that suffix to be omitted.

  5. When the working directory is under some HOL installation's tree (HOLDIR/...), no lastmaker is consulted: the directory hierarchy unambiguously identifies the right Holmake.

  6. It is possible to give a variable a value of pure whitespace by writing NOTHING = ONE_SPACE = $(NOTHING) $(NOTHING)

  7. Note that a --noqof option in a makefile might be overridden from the command-line with the otherwise useless seeming --qof option. In addition, the --no_hmakefile command-line option will stop the makefile from being consulted at all.

Embedding HOL in LaTeX

Embedding HOL in LaTeX

When writing documents in LaTeX about one's favourite HOL development, one frequently wants to include pretty-printed terms, types and theorems from that development. Done manually, this will typically require involved use of the alltt environment, and cutting and pasting from a HOL session or theory file. The result is that one must also keep two copies of HOL texts synchronised: if the HOL development changes, the LaTeX document should change as well.

This manual, and error-prone process is not necessary: the standard HOL distribution comes with a tool called munge.exe to automate the process, and to remove the duplicate copies of HOL text. (Strictly speaking, the distribution comes with a tool that itself creates munge.exe; see Section 10.6.3 below.)

The basic philosophy is that a LaTeX document can be written “as normal”, but that three new LaTeX-like commands are available to the author. The commands are not really processed by LaTeX: instead the source file must first be passed through the munge.exe filter. For example, one might write a document called article.htex. This document contains instances of the new commands, and cannot be processed as is by LaTeX. Instead one first runs

munge.exe < article.htex > article.tex

and then runs LaTeX on article.tex. One would probably automate this process with a makefile of course.

Munging commands

Before starting. In order to use the munger, one must “include” (use the \usepackage command) the holtexbasic.sty style-file, which is found in the HOL source directory src/TeX.

There are then three commands for inserting text corresponding to HOL entities into LaTeX documents: \HOLtm, \HOLty and \HOLthm. Each takes one argument, specifying something of the corresponding HOL type. In addition, options can be specified in square brackets, just as would be done with a genuine LaTeX command. For example, one can write

\HOLtm[tt]{P(SUC n) /\ q}

and one will get $$ \texttt{$P$ (SUC $n$) $\land$ $q$} $$

or something very close to it, appearing in the resulting document.1 Note how the spacing in the input (nothing between the P and the SUC n) is not reflected in the output; this is because the input is parsed and pretty-printed with HOL. This means that if the HOL input is malformed, the munge.exe program will report errors. Note also how the system knows that P, n and q are variables, and that SUC is not. This analysis would not be possible without having HOL actually parse and print the term itself.

The default behaviours of each command are as follows:

\HOLty{string} Parses the string argument as a type (the input must include the leading colon), and prints it. The output is suited for inclusion in the normal flow of LaTeX (it is an \mbox).

\HOLtm{string} Parses the string argument as a term, and prints it. Again, the output is wrapped in an \mbox.

Important: If the string argument includes a right-brace character (i.e., the character }, which has ASCII code 125), then it must be escaped by preceding it with a backslash (\). Otherwise, the munger's lexer will incorrectly determine that the argument ends at that right-brace character rather than at a subsequent one.

\HOLthm{thmspecifier} The argument should be of the form theory.theorem-name. For example, \HOLthm{bool.AND_CLAUSES}. This prints the specified theorem with a leading turnstile.

However, as a special case, if the theorem specified is a “datatype theorem” (with a name of the form datatype_type-name), a BNF-style description of the given type (one that has been defined with Datatype) will be printed. Datatype theorems with these names are automatically generated when Datatype is run. If the trace EmitTeX: print datatypes compactly is set to 1 (see the tr option below) the description is printed in a more compact form. Also, if the type is a collection of nullary constants (a type consisting of only “enumerated constants”), then it will always be printed compactly. When not compact, all of a type's constructors will appear on the same line, or each will be on a separate line. This printing machinery recognises record types (see Section 7.3) and prints them appropriately.

By default, the output is not wrapped in an \mbox, making it best suited for inclusion in an environment such as alltt. (The important characteristics of the alltt environment are that it respects layout in terms of newlines, while also allowing the insertion of LaTeX commands. The verbatim environment does the former, but not the latter.) If using math-mode printing, the output should be included in a holmath environment or similar.

Munging command options. There are a great many options for controlling the behaviour of each of these commands. Some apply to all three commands, others are specific to a subset. If multiple options are desired, they should be separated by commas. For example: \HOLthm[nosp,p/t,>>]{bool.AND_CLAUSES}.

alltt Makes the argument suitable for inclusion in an alltt environment. This is the default for \HOLthm.

case (Only for use with \HOLtm.) Causes the string to be parsed in such a way that any embedded case terms are only partly parsed, allowing their input form to appear when they are output. This preserves underscore-patterns, for example.

conjn (Only for use with \HOLthm.) Extracts the nth conjunct of a theorem. The conjuncts are numbered starting at $1$, not $0$. For example,

\HOLthm[conj3]{bool.AND_CLAUSES}

extracts the conjunct $\vdash \texttt{F} \land t \iff \texttt{F}$.

def (Only for use with \HOLthm.) Causes the theorem to be split into its constituent conjuncts, for each conjunct to have any outermost universal quantifiers removed, and for each to be printed on a line of its own. The turnstiles usually printed in front of theorems are also omitted, and a special form of equality is printed for the top-level (“defining”) equality in each clause. This works well with definitions (or characterising theorems) over multiple data type constructors, changing

⊢ (FACT 0 = 1) ∧ (∀n. FACT (SUC n) = SUC n * FACT n)

into

   FACT 0 ≜ 1
   FACT (SUC n) ≜ SUC n * FACT n

If the special equality is not desired, the option nodefsym can be used to turn this off. The special equality symbol can also be redefined by changing the LaTeX definition of the macro \HOLTokenDefEquality.

There are two variations on the def option:

spaceddef: This option adds extra blank lines between successive conjuncts of a definition.

aligneddef: This option puts ampersands around the \HOLTokenDefEquality macro call in the emitted LaTeX. If the LaTeX environment is an array or similar, this can ensure a nice column-based layout for one's definitions.

Both options can be used together, but aligneddef will not work if nodefsym is also used; if this combination is really required, it would be better to temporarily redefine (use \renewcommand) \HOLTokenDefEquality.

depth=n Causes printing to be done with a maximum print depth of n; see Section 8.1.2.9.

K (Only for use with \HOLtm.) The argument must be the name of a theorem (as per the \HOLthm command), and the theorem should be of the form $$ \vdash f\;x\;t $$ for some term $t$. The command prints the term $t$. The expectation is that $f$ will be the combinator K from combinTheory (see Section 5.2.2), and that $x$ will be truth (T), allowing $t$ to be anything at all. In this way, large complicated terms that are not themselves theorems (or even of boolean type), can be stored in HOL theories, and then printed in LaTeX documents.

m space, nomath The m option makes HOL material be typeset in “math-mode”. In particular, the output of the pretty-printer will be modified so that newline characters are replaced by \\ commands. This then requires that the surrounding LaTeX environment be array-like, so that the \\ command will have the desired effect.

In addition, because raw spaces have minimal effect in math-mode (something like f x will be typeset as $f x$), math-mode munging also replaces spaces with math-mode macros. By default, the command \;\; is used, but if the m option is followed by some characters, each is interpreted as a single-letter macro name, with each macro concatenated together to provide the space command that will be used.

For example, if the option is m;, then the spacing command will be \;. If the option is m;!, then the spacing command will be \;\!. The comma character cannot be used because it conflicts with parsing the list of options, but one can use c instead, so that the option mc will make the spacing command be \,.

The m option can be installed globally with the -m command-line option. If this option is enabled globally, it can be cancelled on a case-by-case basis by using the nomath option. The nomath option also takes precedence over any m options that might occur.

See also the discussion about math-mode munging in Section 10.6.2 below.

merge, nomerge (For use with \HOLtm and \HOLthm.) By default, the HOL pretty-printer is paranoid about token-merging, and will insert spaces between the tokens it emits to try to ensure that what is output can be read in again without error. This behaviour can be frustrating when getting one's LaTeX to look “just so”, so it can be turned off with the nomerge option.

Additionally, this behaviour can be turned off globally with the --nomergeanalysis option to the munger. If this has been made the default, it may be useful to occasionally turn the merge analysis back on for a particular term or theorem; this is done with the merge option. (In interactive HOL, the token-merging analysis is controlled by a trace variable called "pp_avoids_symbol_merges".)

nodollarparens (For use with \HOLtm and \HOLthm.) Causes the default escaping of syntactic sugar to be suppressed. The default behaviour is to use parentheses, so that

\HOLtm{$/\ p}

would get printed as $(\land)\;\,p$. Note that this doesn't reflect the default behaviour in the interactive loop, which is to use dollar-signs (as in the input above); see Section 8.1.2.1. However, with the nodollarparens option specified, nothing at all is printed to indicate that the special syntax has been “escaped”.

nosp (Only for use with \HOLthm.) By default, arguments to \HOLthm are fully specialised (i.e., they have SPEC_ALL applied to them), removing outermost universal quantifiers. The nosp option prevents this.

nostile (Only for use with \HOLthm.) By default, arguments to \HOLthm are printed with a turnstile ($\vdash$). If this option is present, the turnstile is not printed (and the theorem will have its left margin three spaces further left). For controlling how the turnstile is printed when this option is not present, see the paragraph on Overrides in Section 10.6.4.

of (Only for use with \HOLty.) The argument is a string that parses to a term, not a type. The behaviour is to print the type of this term. Thus \HOLty[of]{p /\ q} will print bool.

If the string includes right-braces, they must be escaped with back-slashes, just as with the arguments to \HOLtm.

rule (Only for use with \HOLtm and \HOLthm.) Prints a term (or a theorem's conclusion) using the \infer command (available as part of the proof.sty package). This gives a nice, “natural deduction” presentation. For example, the term

(p \/ q) /\ (p ==> r) /\ (q ==> r) ==> r

will print as $$ \infer{r}{p \lor q & p \Rightarrow r & q \Rightarrow r} $$ Conjuncts to the left of the outermost implication (if any) will be split into hypotheses separated by whitespace. For large rules, this style of presentation breaks down, as there may not be enough horizontal space on the page to fit in all the hypotheses. In this situation, the stackedrule option is appropriate.

The term or theorem must be within a LaTeX math-environment (it is typeset as if inline, with the tt option).

For adding a name to the rule, see the rulename option below.

rulename=name (Only has an effect with rule or stackedrule.) Adds name as the optional argument to the \infer command when typesetting the rule. The name is wrapped with \HOLRuleName, which by default is the same as \textsf. For ease of parsing options, name should not contain braces, brackets, or commas. (A name including such special characters could be typeset by renewing the \HOLRuleName command.)

showtypesn (For use with \HOLthm and \HOLtm.) Causes the term or theorem to be printed with the types trace set to level n. The n is optional and defaults to $1$ if omitted (equivalent to having the show_types reference set to true).

stackedrule (For use with \HOLthm and \HOLtm.) This is similar to the rule option, but causes implication hypotheses to be presented as a “stack”, centered in a LaTeX array on top of one another. Thus,

(p \/ q) /\ (p ==> r) /\ (q ==> r) ==> r

will print as $$ \infer{r}{\begin{array}{c}p \lor q \\ p \Rightarrow r \\ q \Rightarrow r\end{array}} $$ For this purely propositional example with single-letter variable names, the result looks a little odd, but if the hypotheses are textually larger, this option is indispensable.

For adding a name to the rule, see the rulename option.

tr'tracename'=n This option allows the temporary setting of the provided trace to the integer value n. For example, one can set pp_unambiguous_comprehensions to $1$ to ensure that set comprehensions are printed with bound variables explicitly identified. See Section 5.5.1.1 for more on set comprehensions, and Section 10.2 for more on traces.

tt Causes the term to be type-set as the argument to a LaTeX command \HOLinline. By default \HOLinline wraps its argument in an \mbox (keeping it on a single line), applies \textup to force upright shape (even inside math contexts), and sets the contents in typewriter font. See holtexbasic.sty for the exact definition.

This makes the argument suitable for inclusion in standard LaTeX positions. This is the default for \HOLtm and \HOLty. (The \HOLinline command is defined in the holtexbasic.sty style file.)

width=n Causes the argument to be typeset in lines of width n. The default width is $63$, which seems to work well with 11pt fonts. This default can also be changed at the time the munge.exe command is run (see Section 10.6.4 below).

-name This option causes the printing of the term or theorem to be done with respect to a grammar that has all overloading for name removed. When used with \HOLty, prints the type with all type abbreviations for name removed. For example, the command \HOLtm[-+]{x + y} will print as $$ \mathtt{arithmetic\$+}\;x\;y $$ because the underlying constant will no longer map to the string "+" and, in the absence of any other mappings for it, will be printed as a fully qualified name.

If the theory of integers is loaded, then the command \HOLtm[-+]{x + y:int} will print as $$ \mathtt{int\_add}\;x\;y $$ because the mapping from the integer addition constant to "+" is removed, but the mapping to "int_add" remains, allowing that form to be what is printed.

The - option can be useful when complicated notation involving overloads is first introduced in a document.

>> and >>~ Indents the argument. These options only make sense when used with the alltt option (the additional spaces will have no effect when inside an \mbox). The default indentation is two spaces; if a different indentation is desired, the option can be followed by digits specifying the number of space characters desired. For example, \HOLthm[>>10,...]{...} will indent by 10 spaces.

Note that simply placing a command such as \HOLthm within its alltt block with a given indentation, for example

\begin{alltt}
   \HOLthm{bool.AND_CLAUSES}
\end{alltt}

will not do the right thing if the output spans multiple lines. Rather the first line of HOL output will be indented, and the subsequent lines will not. The >> option lets the pretty-printer know that it is printing with a given indentation, affecting all lines of its output.

The version with the tilde character (~) does not add indentation to the first line of output, but adds the specified amount (again 2, if no number is provided) to subsequent lines. This allows one to achieve suitable alignment when other non-HOL text has been put onto the same line. For example,

  AND_CLAUSES \HOLthm[width=46,>>~12]{bool.AND_CLAUSES}
  TRUTH       \HOLthm[>>~12]{bool.TRUTH}
  MAP         \HOLthm[>>~12,width=50]{list.MAP}

ensures correct vertical alignment when extra lines are printed, as they will be with the printing of bool.AND_CLAUSES and list.MAP.

$nm_1$/$nm_2$ (For use with \HOLtm and \HOLthm.) Causes name $nm_1$ to be substituted for name $nm_2$ in the term or theorem. This will rename both free and bound variables, wherever they occur throughout a term. Because it uses instantiation, free variables in theorem hypotheses will get renamed, but bound variables in hypotheses are not affected. (Hypotheses are not printed by default anyway of course.)

If $nm_1$ and $nm_2$ both begin with the colon character then they are parsed as types, and type instantiation is performed on the term or theorem argument instead of variable substitution.

$s$//$t$ (For use with \HOLtm, \HOLthm, and \HOLty) Causes LaTeX string $s$ to be substituted for token $t$. This allows one-off manipulation of the override map (see Section 10.6.4 below). The difference between this operation and the “normal substitution” done with a single slash (as above) is that it happens as the HOL entity is printed, whereas normal substitution happens before pretty-printing is done. If printing depends on particular variable name choices, the “last minute” manipulations possible with this form of substitution may be preferable. The width of the LaTeX string is taken to be the width of the original token $t$.

Math-mode munging

There are a few steps needed to make math-mode munging a relatively painless affair. First, there are two LaTeX macros from holtexbasic.sty that should probably be overridden:

\HOLConst By default this will print names in typewriter font. In math mode, this will probably look better in sans serif, suggesting

\renewcommand{\HOLConst}[1]{\textsf{#1}}

Depending on personal taste, the \HOLKeyword macro might be redefined similarly. This macro is used for keywords such as if.

\HOLinline This macro, used to wrap standard \HOLtm arguments, puts text into typewriter font. One possibility for its redefinition would be

\renewcommand{\HOLinline}[1]{\ensuremath{#1}}

Note that if the term being typeset causes the pretty-printer to break over multiple lines, LaTeX will complain because of the appearance of \\ commands. If necessary, this can be avoided on a case-by-case basis by setting the width option to a larger than normal width.

When using math-mode munging, one also has to be aware of how larger pieces of text will appear. In non-math-mode munging, material is usually put into alltt environments. The recommended alternative for math-mode is to use the \HOLmath environment:

article text

\begin{HOLmath}
\HOLthm{bool.AND_CLAUSES}
\end{HOLmath}

This uses a standard array environment within a displaymath.

Occasionally, one will want to arrange blocks of HOL material within a larger math context. The HOLarray environment is a simple alias for a single-column left-aligned array that one can use in these situations.

Creating a munger

The HOL distribution comes with a tool called mkmunge.exe. This executable is used to create munge executables that behave as described in this section. A typical invocation of mkmunge.exe is

mkmunge.exe <thy_1>Theory ... <thy_n>Theory

Each commandline argument to mkmunge.exe is the name of a HOL object file, so in addition to theory files, one can also include special purpose SML such as monadsyntax.

The mkmunge.exe program can also take an optional -o argument that is used to specify the name of the output munger (the default is munge.exe). For example

mkmunge.exe -o bagtexprocess bagTheory

The theories specified as arguments to mkmunge.exe determine what theorems are in scope for calls to \HOLthm, and also determine the grammars that will govern the parsing and printing of the HOL types, terms and theorems.

Under Poly/ML, the mkmunge.exe executable also takes an optional -b option that can be used to specify a heap (see Section 10.1) to use as a base. Doing so allows for the incorporation of many theories at once, and will be more efficient than loading the heap's theories separately on top of the default HOL heap. The use of a base heap argument to mkmunge.exe doesn't affect the efficiency of the resulting munging tool.

Building and running a munger is typically something that one would want to do under the control of Holmake. A miniature example of such a file is presented in Figure 10.6.3. See Section 10.3 for more on Holmake's capabilities.

Running a munger

Once created, a munger can be run as a filter command, consuming its standard input, and writing to standard output. It may also write error messages and warnings to its standard error.

Thus, a standard pattern of use is something like

munge.exe < article.htex > article.tex

However, there are a number of ways of further modifying the behaviour of the munger with command-line options.

Overrides. Most importantly, one can specify an “overrides file” to provide token-to-LaTeX replacements of what is pretty-printed. The command-line would then look like

munge.exe overrides_file < article.htex > article.tex

The overrides file is a text file containing lines of the form

tok width tex

where tok is a HOL token, width is a number giving the width of the LaTeX, and tex is a LaTeX string.

As a very simple example, an overrides file might consist of just one line:

pi1 2 \ensuremath{\pi_1}

This would cause the string pi1 (presumably occurring in the various HOL entities as a variable name) to be replaced with the rather prettier $\pi_1$. The 2 records the fact that the printer should record the provided LaTeX as being 2 characters wide. This is important for the generation of reasonable line-breaks.

Overrides for HOL tokens can also be provided within HOL theories, using the TeX_notation command (see Section 10.6.6 below).

By overriding the special token $Turnstile$, one can control the printing of the turnstile produced by \HOLthm. The default setup is roughly equivalent to overriding $Turnstile$ to \HOLTokenTurnstile{} followed by a space, giving a total width of 3. Overriding the turnstile in this way will probably be necessary in math-mode printing, where the turnstile character is typically of the same width as 5 \; invocations. Providing the correct width is important in order to get lines past the first to line up with the left edge of the mathematical text rather than the turnstile.

Default width. A munger can specify the default width in which HOL will print its output with a -w option. For example,

munge.exe -w70 < article.htex > article.tex

This default width can be overridden on a case-by-case basis with the width= option to any of the commands within a LaTeX document.

Preventing Merge Analysis. As mentioned above in the description of the merge and nomerge options to the \HOLtm and \HOLthm commands, the munger can be configured to not do token-merging avoidance by passing the --nomergeanalysis option to the munger.

The -w, --nomergeanalysis and overrides file options can be given in any order.

Setting Math-mode Spacing. If one expects to include all of the various \HOL commands in LaTeX math contexts (as described above), then the -m option both sets the default width for math-mode spaces, and also enables math-mode typesetting by default.

The specification of spacing is with a string of characters, as already described. Note that if the command-line option includes any semi-colons or exclamation marks (e.g., -mc;), then they need to be quoted to prevent the shell from getting confused. If the -m option appears without any additional characters, the default math-mode spacing will be \;\;.

Holindex

Till now, it has been explained how the munger can be used as a preprocessor of LaTeX sources. Sometimes a tighter interaction with LaTeX is beneficial. Holindex is a LaTeX package that provides genuine LaTeX commands for inserting HOL-theorems, types and terms as well as many related commands. This allows it to generate an index of all HOL-theorems, types and terms that occur in the document as well as providing citation commands for HOL entities in this index. Holindex can be found in src/TeX/. There is also a demonstration file available in this directory.

Using Holindex. To use Holindex add \usepackage{holindex} to the header of the LaTeX source file article.tex. Holindex loads the underscore package which might cause trouble with references and citations. In order to avoid problems, holindex should be included after packages like natbib. Holindex is used like BibTex or MakeIndex. A run of LaTeX on jobname.tex creates an auxiliary file called article.hix. The munger is used to process this file via

munge.exe -index article

This call generates two additional auxiliary files, article.tde and article.tid. The following runs of LaTeX use these files. After modifying the source file, the munger can be rerun to update article.tde and article.tid. If you are using emacs with AUCTeX to write your latex files, you might want to add

(eval-after-load "tex" '(add-to-list 'TeX-command-list
   '("Holindex" "munge.exe -index %s"
     TeX-run-background t t :help "Run Holindex") t))

to your emacs configuration file. This will allow you to run Holindex using AUCTeX.

Holindex commands.

\blockHOLthm{id}, \blockHOLtm{id}, \blockHOLty{id} These commands typeset the theorem, term or type with the given id as the argument to a LaTeX command \HOLblock. They are intended for typesetting multiple lines in a new block. For theorem ids of the form theory.thm are predefined. All other ids have to be defined before usage as explained below.

\inlineHOLthm{id}, \inlineHOLtm{id}, \inlineHOLty{id} These commands are similar to \blockHOLthm{id}, \blockHOLtm{id} and \blockHOLty{id}. However, they are intended for inline typesetting and use \HOLinline instead of \HOLblock.

\citeHOLthm{id}, \citeHOLtm{id}, \citeHOLty{id} These commands cite a theorem, term or type.

\mciteHOLthm{id,id,...id}, \mciteHOLtm{ids}, \mciteHOLty{ids} These commands cite multiple theorems, terms or types.

\citePureHOLthm{id}, \citePureHOLtm{id}, \citePureHOLty{id} These commands cite a theorems, terms or types. They just typeset the number instead of the verbose form used by the citeHOL and mciteHOL commands.

\citeHiddenHOLthm{id}, \citeHiddenHOLtm{id}, \citeHiddenHOLty{id} These commands cite a theorems, terms or types, but not typeset anything. These commands can be used to add a page to the list of pages a theorem, term or type is cited.

\printHOLIndex, \printHOLShortIndex, \printHOLLongIndex These commands typeset the index of all theorems, terms and types cited in the document. There are two types of entries in the index: long and short ones. Short entries contain a unique number, the label of the theorem, term or type and the pages it is cited. Long entries contain additionally a representation as it would be inserted by \blockHOL... as well as an optional description. Theorems use by default short entries, while terms and types use long ones. It is possible to change for each item whether a long or short entry should be used. \printHOLIndex prints the default index with mixed long and short entries. \printHOLLongIndex typesets just long entries and \printHOLShortIndex just short ones.

Defining and formatting terms, types and theorems.

Most of the Holindex commands require an identifier of a theorem, term or type as arguments. Theorem identifiers of the form theory.theorem are predefined. All other identifiers need defining. Additionally one might want to change the default formatting options for these new identifiers as well as the old ones. HOL definition files can be used for defining and setting the formatting options of identifiers. They are used by putting the command \useHOLfile{filename.hdf} in the header of your latex source file. These file use a syntax similar to BibTex. They consist of a list of entries of the form

@EntryType{id,
  option = value,
  boolFlag,
  ...
}

There are the following entry types:

Thm, Theorem used to define and format a theorem. If the identifier is of the form theory.theorem, the content option can be skipped. Otherwise, the content option should be of this form and a new identifier is defined for the given theorem. This is for example useful if the theorem name contains special characters or if a theorem should be printed with different formatting options.

Term used to define and format a term.

Type used to define and format a type.

Thms, Theorems used to set formatting options for a list of theorems. For example one might want to print long index entries for all theorems in a specific theory. For the Theorems entry the id part of the entry is given in the form ids = [id,id,...]. These ids may be theorem ids or special ids of the form theorem.thmprefix*. For example, the id arithmetic.LESS_EQ* represents all theorems in theory arithmetic whose name starts with LESS_EQ.

Options are name/value pairs. The value has to be quoted using quotation marks or HOL's quotation syntax. There are the following option names available:

content the content. For a term or type that's its HOL definition. For theorems it is of the form theory.theorem.

options formatting options for the munger as described in Section 10.6.1. Please use the Holindex commands for typesetting inline or as a block instead of the options tt or alltt.

label the label that will appear in the index. For theorems the label is by default its name and the label given here will be added after the name.

comment LaTeX code that gets typeset as a comment / description for long index entries.

latex the LaTeX code for the item. There are very rare cases, when it might be useful to provide handwritten LaTeX code instead of the one generated by the munger. This option overrides the LaTeX produced by the munger. It is recommended to use it very carefully.

Besides options, there are also boolean flags that change the formatting of entries:

force-index adds the entry to the index, even if it is not cited in the document.

long-index use a long index-entry.

short-index use a long index-entry.

Here is an example of such a HOL definition file:

@Term{term_id_1,
   content = ``SOME_FUN = SUC a < 0 /\ 0 > SUC b``,
   options = "width=20",
   label = "a short description of term from external file",
   comment = "some lengthy\\comment

              with \textbf{formats} and newlines",
   force_index
}

@Type{type_id_1,
   content = ``:bool``
}

@Thm{arithmetic.LESS_SUCC_EQ_COR,
   force-index, long-index
}

@Thm{thm_1,
   label = "(second instance)",
   content = "arithmetic.LESS_SUC_EQ_COR"
}

@Theorems{
   ids = [arithmetic.LESS_ADD_SUC,
          arithmetic.LESS_EQ*],
   force-index
}

Configuring Holindex.

There are some commands that can be used to change the overall behaviour of Holindex. They should be used in the header directly after holindex is included.

\setHOLlinewidth sets the default line-width. This corresponds to the -w option of the munger.

\setHOLoverrides sets the “overrides file” to provide token-to-LaTeX replacements of what is pretty-printed.

\useHOLfile is used to include a HOL definition file. Several such files might be included.

Additional documentation. For more information about Holindex, please refer to the demonstration file src/TeX/holindex-demo.tex. This file contains documentation for rarely used commands as well as explanations of how to customise Holindex.

Making HOL theories LaTeX-ready

Though one might specify all one's desired token-replacements in an overrides file, there is also support for specifying token replacements in the theory where tokens are first “defined”. (Of course, tokens aren't defined per se, but the definition of particular constants will naturally give rise to the generation of corresponding tokens when those constants appear in HOL terms, types or theorems.)

A token's printing form is given in a script-file with the TeX_notation command (from the TexTokenMap module). This function has type

{ hol : string, TeX : string * int } -> unit

The hol field specifies the string of the token as HOL prints it. The TeX field specifies both the string that should be emitted into the LaTeX output, and the width that this string should be considered to have (as in the overrides file).

For example, in boolScript.sml, there are calls:

val _ = TeX_notation { hol = "!", TeX = ("\\HOLTokenForall{}", 1)}
val _ = TeX_notation { hol = UChar.forall,
                       TeX = ("\\HOLTokenForall{}", 1)}

The UChar structure is a local binding in the script-file that points at the standard list of UTF8-encoded Unicode strings in the distribution (UnicodeChars). Note also how the backslashes that are necessary for the LaTeX command have to be doubled because they are appearing in an SML string.

Finally, rather than mapping the token directly to the string \forall as one might expect, the mapping introduces another level of indirection by mapping to \HOLTokenForall. Bindings for this, and a number of other LaTeX commands are made in the file

src/TeX/holtexbasic.sty

which will need to be included in the LaTeX source file. (Such bindings can be overridden with the use of the command \renewcommand.)

Finally, all theory-bindings made with TeX_notation can be overridden with overrides files referenced at the time a munger is run.


  1. The output is a mixture of typewriter font and math-mode characters embedded in a \texttt block within an \mbox.

References

Abrahamsson, Oskar, and Magnus O. Myreen. 2023. “Fast, Verified Computation for Candle.” In Interactive Theorem Proving (ITP). LIPIcs. https://cakeml.org/itp23.pdf.
Andrews, Peter B. 1986. An Introduction to Mathematical Logic and Type Theory: To Truth Through Proof. Computer Science and Applied Mathematics Series. Academic Press.
Bartle, Robert Gardner. 2001. A Modern Theory of Integration. American Mathematical Soc.
Blanchette, Jasmin Christian, Andrei Popescu, and Dmitriy Traytel. 2014. Cardinals in Isabelle/HOL.” In LNCS 8558 - Interactive Theorem Proving (ITP 2014). Springer. https://doi.org/10.1007/978-3-319-08970-6_8.
Chung, Kai Lai. 2001. A Course in Probability Theory. Third. Academic Press.
Church, Alonzo. 1940. “A Formulation of the Simple Theory of Types.” Journal of Symbolic Logic 5: 56–68.
Coble, Aaron R. 2010. “Anonymity, Information, and Machine-Assisted Proof.” PhD thesis, University of Cambridge.
Feller, William. 2004. An Introduction to Probability Theory and Its Applications. Third. Vol. 1. New York, N.Y.: John Wiley & Sons, Inc.
Fitzpatrick, Patrick, and Halsey Royden. 2010. Real Analysis. 4th ed. Prentice Hall.
Gnedenko, B. V., and A. N. Kolmogorov. 1954. Limit Distributions for Sums of Independent Random Variables. Cambridge, MA: Addison-Wesley Publishing Company, Inc.
Harrison, John. 1998. Theorem-Proving with the Real Numbers. CPHC/BCS Distinguished Dissertations. Springer.
Harrison, John. 2012. The HOL Light Theory of Euclidean Space.” Journal of Automated Reasoning 50 (2): 173–90. https://doi.org/10.1007/s10817-012-9250-9.
Harrison, John. 2017. HOL Light Tutorial. http://www.cl.cam.ac.uk/~jrh13/hol-light/tutorial.pdf.
Hasan, Osman. 2008. “Formal Probabilistic Analysis Using Theorem Proving.” PhD thesis, Concordia University.
Hurd, Joe. 2002. “Formal Verification of Probabilistic Algorithms.” PhD thesis, University of Cambridge.
Kaufmann, Matt, Panagiotis Manolios, and J Strother Moore. 2000. Computer-Aided Reasoning: An Approach. Vol. 3. Advances in Formal Methods. Boston, MA: Springer. https://doi.org/10.1007/978-1-4615-4449-4.
Kolmogorov, A. N. 1950. Foundations of the Theory of Probability. New York: Chelsea Publishing Company.
Leisenring, A. C. 1969. Mathematical Logic and Hilbert’s ϵ-Symbol. University Mathematical Series. London: Macdonald & Co.  Ltd.
Maranget, Luc. 2008. “Compiling Pattern Matching to Good Decision Trees.” In Proceedings of the 2008 ACM SIGPLAN Workshop on ML, 35–46. ML ’08. New York, NY, USA: ACM. https://doi.org/10.1145/1411304.1411311.
Melham, T. F. 1989. “Automating Recursive Type Definitions in Higher Order Logic.” In Current Trends in Hardware Verification and Automated Theorem Proving, edited by G. Birtwistle and P. A. Subrahmanyam, 341–86. Springer-Verlag.
Mhamdi, Tarek. 2012. “Information-Theoretic Analysis Using Theorem Proving.” PhD thesis, Concordia University.
Mhamdi, Tarek, Osman Hasan, and Sofiène Tahar. 2011. “Formalization of Entropy Measures in HOL.” In LNCS 6898 - Interactive Theorem Proving (ITP 2011), 233–48. Berlin, Heidelberg: Springer. https://doi.org/10.1007/978-3-642-22863-6_18.
Milner, Robin. 1989. Communication and Concurrency. PHI Series in Computer Science. Prentice-Hall.
Norrish, Michael, and Brian Huffman. 2013. Ordinals in HOL: Transfinite Arithmetic up to (and Beyond) ω1.” In LNCS 7998 - Interactive Theorem Proving (ITP 2013), 133–46. Springer. https://doi.org/10.1007/978-3-642-39634-2_12.
Paulson, L. 1983. “A Higher-Order Implementation of Rewriting.” Science of Computer Programming 3: 119–49.
Paulson, L. 1987. Logic and Computation: Interactive Proof with Cambridge LCF. Vol. 2. Cambridge Tracts in Theoretical Computer Science. Cambridge University Press.
Qasim, Muhammad. 2016. “Formalization of Normal Random Variables.” PhD thesis, Montréal, Québec, Canada: Concordia University.
Rosenthal, Jeffrey S. 2006. A First Look at Rigorous Probability Theory. Second. Second Edition. World Scientific Publishing Company.
Sangiorgi, Davide. 2012. Introduction to Bisimulation and Coinduction. Cambridge University Press.
Schilling, René L. 2017. Measures, Integrals and Martingales. Second. Cambridge University Press. https://doi.org/10.1017/9781316718339.
Shiryaev, Albert Nikolaevich. 2016. Probability-1. 3rd ed. Vol. 95. Graduate Texts in Mathematics. New York, NY: Springer-Verlag. https://doi.org/10.1007/978-0-387-72206-1.
Slind, Konrad. 1999. “Reasoning about Terminating Functional Programs.” PhD thesis, Technical University of Munich.