ring_exp tactic
A tactic for solving equations in commutative (semi)rings, where the exponents can also contain variables.
More precisely, expressions of the following form are supported:
- constants (non-negative integers)
- variables
- coefficients (any rational number, embedded into the (semi)ring)
- addition of expressions
- multiplication of expressions
- exponentiation of expressions (the exponent must have type
ℕ) - subtraction and negation of expressions (if the base is a full ring)
The motivating example is proving 2 * 2^n * b = b * 2^(n+1),
something that the ring tactic cannot do, but ring_exp can.
Implementation notes
The basic approach to prove equalities is to normalise both sides and check for equality.
The normalisation is guided by building a value in the type ex at the meta level,
together with a proof (at the base level) that the original value is equal to
the normalised version.
The normalised version and normalisation proofs are also stored in the ex type.
The outline of the file:
- Define an inductive family of types
ex, parametrised overex_type, which can represent expressions with+,*,^and rational numerals. The parametrisation overex_typeensures that associativity and distributivity are applied, by restricting which kinds of subexpressions appear as arguments to the various operators. - Represent addition, multiplication and exponentiation in the
extype, thus allowing us to map expressions toex(theevalfunction drives this). We apply associativity and distributivity of the operators here (helped byex_type) and commutativity as well (by sorting the subterms; unfortunately not helped by anything). Any expression not of the above formats is treated as an atom (the same as a variable).
There are some details we glossed over which make the plan more complicated:
- The order on atoms is not initially obvious. We construct a list containing them in order of initial appearance in the expression, then use the index into the list as a key to order on.
- In the tactic, a normalized expression
ps : exlives in the meta-world, but the normalization proofs live in the real world. Thus, we cannot directly sayps.orig = ps.prettyanywhere, but we have to carefully construct the proof when we computeps. This was a major source of bugs in development! - For
pow, the exponent must be a natural number, while the base can be any semiringα. We swap out operations for the base ringαwith those for the exponent ringℕas soon as we deal with exponents. This is accomplished by thein_exponentfunction and is relatively painless since we work in areadermonad. - The normalized form of an expression is the one that is useful for the tactic,
but not as nice to read. To remedy this, the user-facing normalization calls
ex.simp.
Caveats and future work
Subtraction cancels out identical terms, but division does not.
That is: a - a = 0 := by ring_exp solves the goal,
but a / a := 1 by ring_exp doesn't.
Note that 0 / 0 is generally defined to be 0,
so division cancelling out is not true in general.
Multiplication of powers can be simplified a little bit further:
2 ^ n * 2 ^ n = 4 ^ n := by ring_exp could be implemented
in a similar way that 2 * a + 2 * a = 4 * a := by ring_exp already works.
This feature wasn't needed yet, so it's not implemented yet.
Tags
ring, semiring, exponent, power
The atom structure is used to represent atomic expressions:
those which ring_exp cannot parse any further.
For instance, a + (a % b) has a and (a % b) as atoms.
The ring_exp_eq tactic does not normalize the subexpressions in atoms,
but ring_exp does if ring_exp_eq was not sufficient.
Atoms in fact represent equivalence classes of expressions,
modulo definitional equality.
The field index : ℕ should be a unique number for each class,
while value : expr contains a representative of this class.
The function resolve_atom determines the appropriate atom
for a given expression.
We order atoms on the order of appearance in the main expression.
expression section
In this section, we define the ex type and its basic operations.
First, we introduce the supporting types coeff, ex_type and ex_info.
For understanding the code, it's easier to check out ex itself first,
then refer back to the supporting types.
The arithmetic operations on ex need additional definitions,
so they are defined in a later section.
- value : ℚ
Coefficients in the expression are stored in a wrapper structure,
allowing for easier modification of the data structures.
The modifications might be caching of the result of expr.of_rat,
or using a different meta representation of numerals.
- base : tactic.ring_exp.ex_type
- sum : tactic.ring_exp.ex_type
- prod : tactic.ring_exp.ex_type
- exp : tactic.ring_exp.ex_type
The values in ex_type are used as parameters to ex to control the expression's structure.
Each ex stores information for its normalization proof.
The orig expression is the expression that was passed to eval.
The pretty expression is the normalised form that the ex represents.
(I didn't call this something like norm, because there are already
too many things called norm in mathematics!)
The field proof contains an optional proof term of type %%orig = %%pretty.
The value none for the proof indicates that everything reduces to reflexivity.
(Which saves space in quite a lot of cases.)
- zero : tactic.ring_exp.ex_info → tactic.ring_exp.ex tactic.ring_exp.ex_type.sum
- sum : tactic.ring_exp.ex_info → tactic.ring_exp.ex tactic.ring_exp.ex_type.prod → tactic.ring_exp.ex tactic.ring_exp.ex_type.sum → tactic.ring_exp.ex tactic.ring_exp.ex_type.sum
- coeff : tactic.ring_exp.ex_info → tactic.ring_exp.coeff → tactic.ring_exp.ex tactic.ring_exp.ex_type.prod
- prod : tactic.ring_exp.ex_info → tactic.ring_exp.ex tactic.ring_exp.ex_type.exp → tactic.ring_exp.ex tactic.ring_exp.ex_type.prod → tactic.ring_exp.ex tactic.ring_exp.ex_type.prod
- var : tactic.ring_exp.ex_info → tactic.ring_exp.atom → tactic.ring_exp.ex tactic.ring_exp.ex_type.base
- sum_b : tactic.ring_exp.ex_info → tactic.ring_exp.ex tactic.ring_exp.ex_type.sum → tactic.ring_exp.ex tactic.ring_exp.ex_type.base
- exp : tactic.ring_exp.ex_info → tactic.ring_exp.ex tactic.ring_exp.ex_type.base → tactic.ring_exp.ex tactic.ring_exp.ex_type.prod → tactic.ring_exp.ex tactic.ring_exp.ex_type.exp
The ex type is an abstract representation of an expression with +, * and ^.
Those operators are mapped to the sum, prod and exp constructors respectively.
The zero constructor is the base case for ex sum, e.g. 1 + 2 is represented
by (something along the lines of) sum 1 (sum 2 zero).
The coeff constructor is the base case for ex prod, and is used for numerals.
The code maintains the invariant that the coefficient is never 0.
The var constructor is the base case for ex exp, and is used for atoms.
The sum_b constructor allows for addition in the base of an exponentiation;
it serves a similar purpose as the parentheses in (a + b)^c.
The code maintains the invariant that the argument to sum_b is not zero
or sum _ zero.
All of the constructors contain an ex_info field,
used to carry around (arguments to) proof terms.
While the ex_type parameter enforces some simplification invariants,
the following ones must be manually maintained at the risk of insufficient power:
- the argument to
coeffmust be nonzero (to ensure0 = 0 * 1) - the argument to
sum_bmust be of the formsum a (sum b bs)(to ensure(a + 0)^n = a^n) - normalisation proofs of subexpressions must be
refl ps.pretty - if we replace
sumwithconsandzerowithnil, the resulting list is sorted according to theltrelation defined further down; similarly forprodandcoeff(to ensurea + b = b + a).
The first two invariants could be encoded in a subtype of ex,
but aren't (yet) to spare some implementation burden.
The other invariants cannot be encoded because we need the tactic monad to check them.
(For example, the correct equality check of expr is is_def_eq : expr → expr → tactic unit.)
Return the proof information associated to the ex.
Return the original, non-normalized version of this ex.
Note that arguments to another ex are always "pre-normalized":
their orig and pretty are equal, and their proof is reflexivity.
Return the normalized version of this ex.
Return the normalisation proof of the given expression.
If the proof is refl, we give none instead,
which helps to control the size of proof terms.
To get an actual term, use ex.proof_term,
or use mk_proof with the correct set of arguments.
Update the orig and proof fields of the ex_info.
Intended for use in ex.set_info.
Update the ex_info of the given expression.
We use this to combine intermediate normalisation proofs.
Since pretty only depends on the subexpressions,
which do not change, we do not set pretty.
Equations
- tactic.ring_exp.coeff_has_repr = {repr := λ (x : tactic.ring_exp.coeff), repr x.value}
Convert an ex to a string.
Equality test for expressions.
Since equivalence of atoms is not the same as equality,
we cannot make a true (=) operator for ex either.
The ordering on expressions.
As for ex.eq, this is a linear order only in one context.
operations section
This section defines the operations (on ex) that use tactics.
They live in the ring_exp_m monad,
which adds a cache and a list of encountered atoms to the tactic monad.
Throughout this section, we will be constructing proof terms. The lemmas used in the construction are all defined over a commutative semiring α.
- α : expr
- univ : level
- csr_instance : expr
- ha_instance : expr
- hm_instance : expr
- hp_instance : expr
- ring_instance : option expr
- dr_instance : option expr
- zero : expr
- one : expr
Stores the information needed in the eval function and its dependencies,
so they can (re)construct expressions.
The eval_info structure stores this information for one type,
and the context combines the two types, one for bases and one for exponents.
- info_b : tactic.ring_exp.eval_info
- info_e : tactic.ring_exp.eval_info
- transp : tactic.transparency
The context contains the full set of information needed for the eval function.
This structure has two copies of eval_info:
one is for the base (typically some semiring α) and another for the exponent (always ℕ).
When evaluating an exponent, we put info_e in info_b.
The ring_exp_m monad is used instead of tactic to store the context.
Access the instance cache.
Lift an operation in the tactic monad to the ring_exp_m monad.
This operation will not access the cache.
Change the context of the given computation, so that expressions are evaluated in the exponent ring, instead of the base ring.
Specialized version of mk_app where the first two arguments are {α} [some_class α].
Should be faster because it can use the cached instances.
Specialized version of mk_app where the first two arguments are {α} [comm_semiring α].
Should be faster because it can use the cached instances.
Specialized version of mk_app ``has_add.add.
Should be faster because it can use the cached instances.
Specialized version of mk_app ``has_mul.mul.
Should be faster because it can use the cached instances.
Specialized version of mk_app ``has_pow.pow.
Should be faster because it can use the cached instances.
Construct a normalization proof term or return the cached one.
Construct a normalization proof term or return the cached one.
If all ex_info have trivial proofs, return a trivial proof.
Otherwise, construct all proof terms.
Useful in applications where trivial proofs combine to another trivial proof,
most importantly to pass to mk_proof_or_refl.
Use the proof terms as arguments to the given lemma.
If the lemma could reduce to reflexivity, consider using mk_proof_or_refl.
Use the proof terms as arguments to the given lemma. Often, we construct a proof term using congruence where reflexivity suffices. To solve this, the following function tries to get away with reflexivity.
A shortcut for adding the original terms of two expressions.
A shortcut for multiplying the original terms of two expressions.
A shortcut for exponentiating the original terms of two expressions.
Congruence lemma for constructing ex.sum.
Congruence lemma for constructing ex.prod.
Congruence lemma for constructing ex.exp.
Constructs ex.zero with the correct arguments.
Constructs ex.sum with the correct arguments.
Constructs ex.coeff with the correct arguments.
There are more efficient constructors for specific numerals:
if x = 0, you should use ex_zero; if x = 1, use ex_one.
Constructs ex.coeff 1 with the correct arguments.
This is a special case for optimization purposes.
Constructs ex.prod with the correct arguments.
Constructs ex.var with the correct arguments.
Constructs ex.sum_b with the correct arguments.
Constructs ex.exp with the correct arguments.
Conversion from ex base to ex exp.
Conversion from ex exp to ex prod.
A more efficient conversion from atom to ex sum.
The result should be the same as ex_var p >>= base_to_exp >>= exp_to_prod >>= prod_to_sum,
except we need to calculate less intermediate steps.
Compute the sum of two coefficients.
Note that the result might not be a valid expression:
if p = -q, then the result should be ex.zero : ex sum instead.
The caller must detect when this happens!
The returned value is of the form ex.coeff _ (p + q),
with the proof of expr.of_rat p + expr.of_rat q = expr.of_rat (p + q).
Compute the product of two coefficients.
The returned value is of the form ex.coeff _ (p * q),
with the proof of expr.of_rat p * expr.of_rat q = expr.of_rat (p * q).
- none : tactic.ring_exp.overlap
- nonzero : tactic.ring_exp.ex tactic.ring_exp.ex_type.prod → tactic.ring_exp.overlap
- zero : tactic.ring_exp.ex tactic.ring_exp.ex_type.sum → tactic.ring_exp.overlap
Represents the way in which two products are equal except coefficient.
This type is used in the function add_overlap.
In order to deal with equations of the form a * 2 + a = 3 * a,
the add function will add up overlapping products,
turning a * 2 + a into a * 3.
We need to distinguish a * 2 + a from a * 2 + b in order to do this,
and the overlap type carries the information on how it overlaps.
The case none corresponds to non-overlapping products, e.g. a * 2 + b;
the case nonzero to overlapping products adding to non-zero, e.g. a * 2 + a
(the ex prod field will then look like a * 3 with a proof that a * 2 + a = a * 3);
the case zero to overlapping products adding to zero, e.g. a * 2 + a * -2.
We distinguish those two cases because in the second, the whole product reduces to 0.
A potential extension to the tactic would also do this for the base of exponents,
e.g. to show 2^n * 2^n = 4^n.
Given arguments ps, qs of the form ps' * x and ps' * y respectively
return ps + qs = ps' * (x + y) (with x and y arbitrary coefficients).
For other arguments, return overlap.none.
Add two expressions.
0 + qs = 0ps + 0 = 0ps * x + ps * y = ps * (x + y)(forx,ycoefficients; usesadd_overlap)(p + ps) + (q + qs) = p + (ps + (q + qs))(ifp.lt q)(p + ps) + (q + qs) = q + ((p + ps) + qs)(if notp.lt q)
Multiply two expressions.
x * y = (x * y)(forx,ycoefficients)x * (q * qs) = q * (qs * x)(forxcoefficient)(p * ps) * y = p * (ps * y)(forycoefficient)(p_b^p_e * ps) * (p_b^q_e * qs) = p_b^(p_e + q_e) * (ps * qs)(ifp_eandq_eare identical except coefficient)(p * ps) * (q * qs) = p * (ps * (q * qs))(ifp.lt q)(p * ps) * (q * qs) = q * ((p * ps) * qs)(if notp.lt q)
Multiply two expressions.
0 * qs = 0(p + ps) * qs = (p * qs) + (ps * qs)
Multiply two expressions.
ps * 0 = 0ps * (q + qs) = (ps * q) + (ps * qs)
Compute the exponentiation of two coefficients.
The returned value is of the form ex.coeff _ (p ^ q),
with the proof of expr.of_rat p ^ expr.of_rat q = expr.of_rat (p ^ q).
Exponentiate two expressions.
1 ^ qs = 1x ^ qs = x ^ qs(forxcoefficient)(p * ps) ^ qs = p ^ qs + ps ^ qs
Exponentiate two expressions.
ps ^ 1 = ps0 ^ qs = 0(note that this is handled afterps ^ 0 = 1)(p + 0) ^ qs = p ^ qsps ^ (qs + 1) = ps * ps ^ qs(note that this is handled afterp + 0 ^ qs = p ^ qs)ps ^ qs = ps ^ qs(otherwise)
Exponentiate two expressions.
ps ^ 0 = 1ps ^ (q + qs) = ps ^ q * ps ^ qs
Give a simpler, more human-readable representation of the normalized expression.
Normalized expressions might have the form a^1 * 1 + 0,
since the dummy operations reduce special cases in pattern-matching.
Humans prefer to read a instead.
This tactic gets rid of the dummy additions, multiplications and exponentiations.
Performs a lookup of the atom a in the list of known atoms,
or allocates a new one.
If a is not definitionally equal to any of the list's entries,
a new atom is appended to the list and returned.
The index of this atom is kept track of in the second inductive argument.
This function is mostly useful in resolve_atom,
which updates the state with the new list of atoms.
Convert the expression to an atom: either look up a definitionally equal atom, or allocate it as a new atom.
You probably want to use eval_base if eval doesn't work
instead of directly calling resolve_atom,
since eval_base can also handle numerals.
Treat the expression atomically: as a coefficient or atom.
Handles cases where eval cannot treat the expression as a known operation
because it is just a number or single variable.
Negate an expression by multiplying with -1.
Only works if there is a ring instance; otherwise it will fail.
Invert an expression by simplifying, applying has_inv.inv and treating the result as an atom.
Only works if there is a division_ring instance; otherwise it will fail.
wiring section
This section deals with going from expr to ex and back.
The main attraction is eval, which uses add, mul, etc.
to calculate an ex from a given expr.
Other functions use exes to produce exprs together with a proof,
or produce the context to run ring_exp_m from an expr.
Compute a normalized form (of type ex) from an expression (of type expr).
This is the main driver of the ring_exp tactic,
calling out to add, mul, pow, etc. to parse the expr.
Run eval on the expression and return the result together with normalization proof.
See also eval_simple if you want something that behaves like norm_num.
Run eval on the expression and simplify the result.
Returns a simplified normalized expression, together with an equality proof.
See also eval_with_proof if you just want to check the equality of two expressions.
Compute the eval_info for a given type α.
Use e to build the context for running mx.
Repeatedly apply eval_simple on (sub)expressions.
Tactic for solving equations of commutative (semi)rings,
allowing variables in the exponent.
This version of ring_exp fails if the target is not an equality.
The variant ring_exp_eq! will use a more aggressive reducibility setting
to determine equality of atoms.
Tactic for evaluating expressions in commutative (semi)rings, allowing for variables in the exponent.
This tactic extends ring: it should solve every goal that ring can solve.
Additionally, it knows how to evaluate expressions with complicated exponents
(where ring only understands constant exponents).
The variants ring_exp! and ring_exp_eq! use a more aggessive reducibility setting to determine
equality of atoms.
For example:
Normalises expressions in commutative (semi-)rings inside of a conv block using the tactic ring_exp.