The ring_exp tactic uses a normalisation scheme similar to the original ring tactic. The input from the tactic system is an abstract syntax tree representing the expression to normalise. An eval function maps inputs to a type ex of normalised expressions. The normal form should be designed in such a way that values of type ex are equal if and only if the input expressions can be proved equal using the axioms of commutative semirings. From the ex representation, the normalised output expression is constructed by a function simple. Both eval and simple additionally return a proof showing that the input and output expressions are equal.
The ring_exp tactic does not use reflection but directly constructs proof terms to be type checked by Lean’s kernel, as is typical for tactics in mathlib [10]. Reflective tactics avoid the construction and checking of a large proof term by performing most computation during proof checking, running a verified program [2]. If the proof checker performs efficient reduction, this results in a significant speed-up of the tactic, at the same time as providing more correctness guarantees. Unfortunately, the advantages of reflection do not translate directly to Lean. Tactic execution in Lean occurs within a fast interpreter, while the kernel used in proof checking is designed for simplicity instead of efficient reduction [3]. Achieving an acceptable speed for ring_exp requires other approaches to the benefits that reflection brings automatically.
The language of semirings implemented by ring, with binary operators \(+\), \(*\) and optionally − and /, is extended in ring_exp with a binary exponentiation operator \({}^\wedge \). The input expression can consist of these operators applied to other expressions, with two base cases: natural numerals such as 0 and 37, and atoms. An atom is any expression which is not of the above form, e.g. a variable name x or a function application \(\sin (y - z)\). It is treated as an opaque variable in the expression. Two such expressions are considered equal if in every commutative semiring they evaluate to equal values, for any assignment to the atoms.
Using a suitable representation of the normal form is crucial to easily guarantee correctness of the normaliser. Since there is no clear way to generalise the Horner form, ring_exp instead represents its normal form ex as a tree with operators at the nodes and atoms at the leaves. Certain classes of non-normalised expressions are prohibited by restricting which sub-node can occur for each node. The ex type captures these restrictions through a parameter in the enum ex_type, creating an inductive family of types. Each constructor allows specific members of the ex family in its arguments and returns a specific type of ex. The full definition is given in Fig. 1. The additional ex_info record passed to the constructors contains auxiliary information used to construct correctness proofs. The sum_b constructor allows sums as the base of a power, analogously to the parentheses in \((a + b) ^ c\).
For readability, we will write the ex representation in symbols instead of the constructors of ex. Thus, the term sum (prod (exp (var n) (coeff 1)) (coeff 1)) zero (with ex_info fields omitted) is written as \(n^1 * 1 + 0\), and the normalised form of \(2^n - 1\) is written \((2+0)^{n^1 * 1} * 1 + (-1) + 0\).
Table 1. Associativity and distributivity properties of the \(+\), \(*\) and \({}^\wedge \) operators
The types of the arguments to each constructor are determined by the associativity and distributivity properties of the operators involved, summarised in Table 1. Since addition does not distribute over either other operator (as seen from the empty entries on the \(+\) row), an expression with a sum as outermost operator cannot be rewritten so that another operator is outermost. Thus, the set of all expressions should be represented by ex sum. Since \(*\) distributes over \(+\) but not over \({}^\wedge \), the next outermost operator after \(+\) will be \(*\). By associativity (the diagonal entries of the table) the left argument to \(+\) should have \(*\) as outermost operator; otherwise we can apply the rewrite rule \((a + b) + c \mapsto a + (b + c)\). Analogously, the left argument to the prod constructor is not an ex prod but an ex exp, and the left argument to exp is an ex base.
The eval function interprets each operator in the input expression as a corresponding operation on ex, building a normal form for the whole expression out of normalised subexpressions. The operations on ex build the correctness proof of normalisation out of the proofs for subexpressions using a correctness lemma: for example, the lemma
is used on the input expression ps + qs when ps normalises to 0.
Adding support for a new operator would take relatively little work: after extending the table of associativity and distributivity relations, one can insert the constructor in ex using the table to determine the relevant ex_type, and add an operation on ex that interprets the operator.