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_tacalso repeatedly “strips” the goal in the same way as the high-levelrwtactic (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" simpset — srw_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.
| Fragment | Description |
|---|---|
ARITH_ss | Embodies decision procedure for universal Presburger arithmetic over $\mathbb{N}$. (Defined in numSimps; used in high-level simplification tactics.) |
BOOL_ss | Standard rewrites for the boolean operators (conjunction, negation etc.), and conversion for performing $\beta$-reduction. (Defined in boolSimps; part of bool_ss.) |
CONG_ss | Congruence rules for implication and conditional expressions. (Defined in boolSimps; part of bool_ss.) |
CONJ_ss | Lets 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_ss | Normalises to disjunctive normal form; quantifiers moved for elimination over equalities. (Defined in boolSimps.) |
ETA_ss | Eliminates 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
- 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.)
- 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).
- 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.