module Data.Nat.Solver where

The Nat solverπŸ”—

This module defines a solver for equations in the commutative semiring of natural numbers. Our approach splits cleanly into 3 distinct parts:

  • Horner normal forms for polynomials
  • Evaluation of reflected terms into polynomials
  • The reflection interface

Horner normal formsπŸ”—

If we ignore the suc and zero constructors, and their respective equations, the core problem at hand is trying to compute normal forms for polynomials. Luckily, like most problems involving polynomials, this has been thoroughly studied! There are many possible normal forms to choose from, but the most useful for our task is Horner normal form, as it admits a particularly nice inductive characterization.

The core idea is that we can rewrite a (univariate) polynomial of the form into the following form:

However, we need multivariate polynomials, not just univariate! To do this, we exploit the isomorphism between and This allows us to define the normal form of a multivariate polynomial as (essentially) a product of univariate ones.

We start by defining the type of n-ary multivariate polynomials.

data Poly {a} (A : Type a) : Nat β†’ Type a where

The first polynomial we define is the constant polynomial

  const : (c : A) β†’ Poly A 0

Next, we define the 0 polynomial Note that we do _not. include here! This is important for ensuring that our data type defines a (somewhat) unique normal form. If we had instead chosen Poly A n, we could represent the polynomial using either const 0 or zerop, which complicates matters somewhat.

  zerop  : βˆ€ {n} β†’ Poly A (suc n)

Finally, we define both addition and multiplication by in one fell swoop. This constructor represents the polynomial where and

This flipping of the index may seem confusing at first, but it serves an important purpose: it makes evaluation really easy! When evaluating, we will need some environment Vec A n which assigns values to the various indeterminates In the variable case, the indexing will ensure that our environment is non-empty, whence we can use the tail of the vector to evaluate q! In this sense, not only does this constructor handle addition and multiplication by it also handles weakening.

  _*X+_ : βˆ€ {n} β†’ (p : Poly A (suc n)) β†’ (q : Poly A n) β†’ Poly A (suc n)

Note that this representation, while convenient, does pose some problems. We pay for the (relative) uniqueness by having larger terms: For instance, the polynomial is represented as

private
  x⁴+1 : Poly Nat 1
  x⁴+1 = zerop *X+ const 1 *X+ const 0 *X+ const 0 *X+ const 0 *X+ const 0

This could be alleviated by using a sparse representation, but this is decidedly more difficult. As the solver is not expected to deal with polynomials with large degrees, this term blow-up will not be a problem in practice.

Operations on Horner normal formsπŸ”—

Now, let’s define a handful of functions for constructing and combining polynomials. The naming here can get a bit confusing, so let’s stick with the convention of adding a subscript p to denote an operation on polynomials. As a further note, the entire following section could be generalized work over an arbitrary semiring, but this would complicate the dependency graph somewhat, so we stick to natural numbers.

As previously mentioned, we have different representations of the polynomial depending on if we are working with Poly A zero or Poly A (suc n). This is somewhat annoying, so we define a small helper for picking the correct representation.

0β‚š : Poly Nat n
0β‚š {n = zero}  = const zero
0β‚š {n = suc n} = zerop

While we are at it, we also define the polynomial that represents, given a constant the constant polynomial

constβ‚š : Nat β†’ Poly Nat n
constβ‚š {n = zero}  c = const c
constβ‚š {n = suc n} c = 0β‚š *X+ constβ‚š c

The constant 1 is important enough that it deserves its own syntax.

1β‚š : Poly Nat n
1β‚š = constβ‚š 1

We also define the identity monomials

X[_] : Fin n β†’ Poly Nat n
X[_] fzero    = 1β‚š *X+ 0β‚š
X[_] (fsuc i) = 0β‚š *X+ X[ i ]

Now, onto addition of polynomials. This is more or less what one would expect if they wrote out the polynomials and did the addition by hand.

infixl 6 _+β‚š_
infixl 7 _*β‚š_

_+β‚š_ : Poly Nat n β†’ Poly Nat n β†’ Poly Nat n
const c₁ +β‚š const cβ‚‚ = const (c₁ + cβ‚‚)
zerop +β‚š q = q
(p *X+ q) +β‚š zerop = p *X+ q
(p *X+ r) +β‚š (q *X+ s) = (p +β‚š q) *X+ (r +β‚š s)

Multiplication, however, is somewhat trickier. The problem is that during the course of recursion, we will need to multiply a Poly A n by a Poly A (suc n) β€” for which we will need mutual recursion, since that multiplication will fall back to the β€œhomogeneous” case eventually. We predeclare their types to make Agda happy:

_*β‚š_ : Poly Nat n β†’ Poly Nat n β†’ Poly Nat n
_*β‚š'_ : Poly Nat n β†’ Poly Nat (suc n) β†’ Poly Nat (suc n)

First, the homogeneous multiplication. The first two cases are pretty straightforward, but the final one is decidedly less so. To start, we can distribute the multiplication of r over the addition, invoking _+β‚š_ to add the results together. When multiplying q and r, we need to use the aforementioned heterogeneous multiplication, as β€” note that the index is off by one ( vs When multiplying p and r, we need to add on a 0β‚š under the *X, as this is the only way of multiplying by

const c₁  *β‚š const cβ‚‚ = const (c₁ * cβ‚‚)
zerop     *β‚š q        = zerop
(p *X+ q) *β‚š r        = ((p *β‚š r) *X+ 0β‚š) +β‚š (q *β‚š' r)

For the heterogeneous case, the call graph is simpler, as we can fall back to the homogeneous operator.

r *β‚š' zerop     = zerop
r *β‚š' (p *X+ q) = (r *β‚š' p) *X+ (r *β‚š q)

Evaluation of Horner normal formsπŸ”—

Multivariate polynomials represent functions so we should be able to interpret them as such. Luckily, Horner Normal Forms are extremely easy to evaluate. As a historical note, this is why this representation was created in first place! In this light, they should probably be called β€œSharaf al-Din al-Tusi normal forms”.

block : Nat β†’ Nat
block x = x
⟦_βŸ§β‚š : Poly Nat n β†’ Vec Nat n β†’ Nat
⟦ const c βŸ§β‚š env        = c
⟦ zerop βŸ§β‚š   env        = 0
⟦ p *X+ q βŸ§β‚š (xβ‚€ ∷ env) = ⟦ p βŸ§β‚š (xβ‚€ ∷ env) * xβ‚€ + ⟦ q βŸ§β‚š env

Soundness of the operationsπŸ”—

Now, it’s important that the operations we defined actually denote the correct operations on natural numbers. As a warm up, let’s show that the zero polynomial really represents the function

sound-0β‚š : βˆ€ (env : Vec Nat n) β†’ ⟦ 0β‚š βŸ§β‚š env ≑ 0
sound-0β‚š []        = refl
sound-0β‚š (x ∷ env) = refl

We do the same for the constant polynomials:

sound-constβ‚š : βˆ€ c β†’ (env : Vec Nat n) β†’ ⟦ constβ‚š c βŸ§β‚š env ≑ c
sound-constβ‚š c [] = refl
sound-constβ‚š c (x ∷ env) = sound-constβ‚š c env

At the risk of repeating ourselves, we also show the same for the monomial

sound-X[_] : βˆ€ i β†’ (env : Vec Nat n) β†’ ⟦ X[ i ] βŸ§β‚š env ≑ lookup env i
sound-X[ fzero ] (xβ‚€ ∷ env) =
  ⟦ constβ‚š 1 βŸ§β‚š env * xβ‚€ + ⟦ 0β‚š βŸ§β‚š env β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ Ο• * xβ‚€ + ψ) (sound-constβ‚š 1 env) (sound-0β‚š env) βŸ©β‰‘
  1 * xβ‚€ + 0                           β‰‘βŸ¨ +-zeror (1 * xβ‚€) βŸ©β‰‘
  1 * xβ‚€                               β‰‘βŸ¨ *-onel xβ‚€ βŸ©β‰‘
  xβ‚€ ∎
sound-X[ fsuc i ] (_ ∷ env) = sound-X[ i ] env

Now, for something more involved: let’s show that addition of polynomials really deserves the name β€œaddition” β€” under the semantics mapping ⟦_βŸ§β‚š, adding two polynomials then evaluating is the same thing as evaluating each then adding the results.

sound-+β‚š : βˆ€ p q β†’ (env : Vec Nat n)
           β†’ ⟦ p +β‚š q βŸ§β‚š env ≑ ⟦ p βŸ§β‚š env + ⟦ q βŸ§β‚š env
sound-+β‚š (const c1) (const c2) env = refl
sound-+β‚š zerop q env = refl
sound-+β‚š (p *X+ r) zerop env =
  sym (+-zeror (⟦ (p *X+ r) +β‚š zerop βŸ§β‚š env))
sound-+β‚š (p *X+ r) (q *X+ s) (xβ‚€ ∷ env) =
  ⟦p+q⟧ * xβ‚€ + ⟦r+s⟧                β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ Ο• * xβ‚€ + ψ) (sound-+β‚š p q (xβ‚€ ∷ env)) (sound-+β‚š r s env) βŸ©β‰‘
  (⟦p⟧ + ⟦q⟧) * xβ‚€ + (⟦r⟧ + ⟦s⟧)    β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• + (⟦r⟧ + ⟦s⟧)) (*-distrib-+r ⟦p⟧ ⟦q⟧ xβ‚€) βŸ©β‰‘
  ⟦p⟧ * xβ‚€ + ⟦q⟧ * xβ‚€ + (⟦r⟧ + ⟦s⟧) β‰‘βŸ¨ commute-inner (⟦p⟧ * xβ‚€) (⟦q⟧ * xβ‚€) ⟦r⟧ ⟦s⟧ βŸ©β‰‘
  ⟦p⟧ * xβ‚€ + ⟦r⟧ + (⟦q⟧ * xβ‚€ + ⟦s⟧) ∎
  where
    ⟦p+q⟧ = ⟦ p +β‚š q βŸ§β‚š (xβ‚€ ∷ env)
    ⟦r+s⟧ = ⟦ r +β‚š s βŸ§β‚š env
    ⟦p⟧ = ⟦ p βŸ§β‚š (xβ‚€ ∷ env)
    ⟦r⟧ = ⟦ r βŸ§β‚š env
    ⟦q⟧ = ⟦ q βŸ§β‚š (xβ‚€ ∷ env)
    ⟦s⟧ = ⟦ s βŸ§β‚š env

Wow, that was a bit painful! This is a common theme when writing proof automation: it distills down a lot of the annoying proofs we need to write across the entire code-base into one extremely painful proof. Thus, conservation of frustration is preserved.

Philosophical reflections aside, let’s move onto multiplication of polynomials. As the homogeneous and heterogeneous multiplication were defined in a mutually recursive manner, we must do so for their proofs of soundness as well.

sound-*β‚š
  : βˆ€ p q β†’ (env : Vec Nat n)
  β†’ ⟦ p *β‚š q βŸ§β‚š env ≑ ⟦ p βŸ§β‚š env * ⟦ q βŸ§β‚š env
sound-*β‚š'
  : βˆ€ p q β†’ (xβ‚€ : Nat) β†’ (env : Vec Nat n)
  β†’ ⟦ p *β‚š' q βŸ§β‚š (xβ‚€ ∷ env) ≑ ⟦ p βŸ§β‚š env * ⟦ q βŸ§β‚š (xβ‚€ ∷ env)

The first couple of cases of homogeneous multiplication don’t look so bad…

sound-*β‚š (const c1) (const c2) env = refl
sound-*β‚š zerop q env = refl
sound-*β‚š (p *X+ r) zerop (xβ‚€ ∷ env) =
  ⟦ p *β‚š zerop βŸ§β‚š (xβ‚€ ∷ env) * xβ‚€ + ⟦ 0β‚š βŸ§β‚š env β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ Ο• * xβ‚€ + ψ) (sound-*β‚š p zerop (xβ‚€ ∷ env)) (sound-0β‚š env) βŸ©β‰‘
  ⟦p⟧ * 0 * xβ‚€ + 0                              β‰‘βŸ¨ ap (Ξ» Ο• β†’ (Ο• * xβ‚€) + 0) (*-zeror ⟦p⟧) βŸ©β‰‘
  0                                             β‰‘Λ˜βŸ¨ *-zeror (⟦p⟧ * xβ‚€ + ⟦r⟧) βŸ©β‰‘Λ˜
  (⟦p⟧ * xβ‚€ + ⟦r⟧) * 0 ∎
  where
    ⟦p⟧ = ⟦ p βŸ§β‚š (xβ‚€ ∷ env)
    ⟦r⟧ = ⟦ r βŸ§β‚š env

However, the case where we need to distribute is not so easy. The consists of repeatedly expanding out the polynomial operations into those on natural numbers, then doing a brutal bit of symbol shuffling. There’s not too much to be gained from dwelling on this, so let’s move on.

sound-*β‚š (p *X+ r) (q *X+ s) (xβ‚€ ∷ env) =
  ⟦p*⟨qx+s⟩+r*q⟧ * xβ‚€ + ⟦ 0β‚š +β‚š (r *β‚š s) βŸ§β‚š env                β‰‘βŸ¨ ap (Ξ» Ο• β†’ ⟦p*⟨qx+s⟩+r*q⟧ * xβ‚€ + Ο•) (sound-+β‚š 0β‚š (r *β‚š s) env) βŸ©β‰‘
  ⟦p*⟨qx+s⟩+r*q⟧ * xβ‚€ + (⟦ 0β‚š βŸ§β‚š env + ⟦ r *β‚š s βŸ§β‚š env)        β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ ⟦p*⟨qx+s⟩+r*q⟧ * xβ‚€ + (Ο• + ψ)) (sound-0β‚š env) (sound-*β‚š r s env) βŸ©β‰‘
  ⟦p*⟨qx+s⟩+r*q⟧ * xβ‚€ + (⟦r⟧ * ⟦s⟧)                            β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• * xβ‚€ + ⟦r⟧ * ⟦s⟧) (sound-+β‚š (p *β‚š (q *X+ s)) (r *β‚š' q) (xβ‚€ ∷ env)) βŸ©β‰‘
  (⟦p*⟨qx+s⟩⟧ + ⟦r*q⟧) * xβ‚€ + ⟦r⟧ * ⟦s⟧                        β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ (Ο• + ψ) * xβ‚€ + ⟦r⟧ * ⟦s⟧) (sound-*β‚š p (q *X+ s) (xβ‚€ ∷ env)) (sound-*β‚š' r q xβ‚€ env) βŸ©β‰‘
  (⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) + ⟦r⟧ * ⟦q⟧) * xβ‚€ + ⟦r⟧ * ⟦s⟧        β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• + ⟦r⟧ * ⟦s⟧) (*-distrib-+r (⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧)) (⟦r⟧ * ⟦q⟧) xβ‚€) βŸ©β‰‘
  ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + ⟦r⟧ * ⟦q⟧ * xβ‚€ + ⟦r⟧ * ⟦s⟧     β‰‘Λ˜βŸ¨ +-associative (⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€) (⟦r⟧ * ⟦q⟧ * xβ‚€) (⟦r⟧ * ⟦s⟧) βŸ©β‰‘Λ˜
  ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + (⟦r⟧ * ⟦q⟧ * xβ‚€ + ⟦r⟧ * ⟦s⟧)   β‰‘βŸ¨ ap (Ξ» Ο• β†’  ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + (Ο• + ⟦r⟧ * ⟦s⟧)) (*-associative ⟦r⟧ ⟦q⟧ xβ‚€) βŸ©β‰‘
  ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + (⟦r⟧ * (⟦q⟧ * xβ‚€) + ⟦r⟧ * ⟦s⟧) β‰‘Λ˜βŸ¨ ap (Ξ» Ο• β†’ ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + Ο•) (*-distrib-+l (⟦q⟧ * xβ‚€) ⟦s⟧ ⟦r⟧) βŸ©β‰‘Λ˜
  ⟦p⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧) * xβ‚€ + ⟦r⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧)         β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• + ⟦r⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧)) (commute-last ⟦p⟧ (⟦q⟧ * xβ‚€ + ⟦s⟧) xβ‚€) βŸ©β‰‘
  ⟦p⟧ * xβ‚€ * (⟦q⟧ * xβ‚€ + ⟦s⟧) + ⟦r⟧ * (⟦q⟧ * xβ‚€ + ⟦s⟧)         β‰‘Λ˜βŸ¨ *-distrib-+r (⟦p⟧ * xβ‚€) ⟦r⟧ (⟦q⟧ * xβ‚€ + ⟦s⟧) βŸ©β‰‘Λ˜
  (⟦p⟧ * xβ‚€ + ⟦r⟧) * (⟦q⟧ * xβ‚€ + ⟦s⟧)                          ∎
  where
    ⟦p*⟨qx+s⟩+r*q⟧ = ⟦ (p *β‚š (q *X+ s)) +β‚š (r *β‚š' q) βŸ§β‚š (xβ‚€ ∷ env)
    ⟦p*⟨qx+s⟩⟧ = ⟦ p *β‚š (q *X+ s) βŸ§β‚š (xβ‚€ ∷ env)
    ⟦r*q⟧ = ⟦ r *β‚š' q βŸ§β‚š (xβ‚€ ∷ env)
    ⟦p⟧ = ⟦ p βŸ§β‚š (xβ‚€ ∷ env)
    ⟦r⟧ = ⟦ r βŸ§β‚š env
    ⟦q⟧ = ⟦ q βŸ§β‚š (xβ‚€ ∷ env)
    ⟦s⟧ = ⟦ s βŸ§β‚š env

As a nice palate cleanser, the proofs for heterogeneous multiplication are nowhere near as bad.

sound-*β‚š' p zerop xβ‚€ env = sym (*-zeror (⟦ p βŸ§β‚š env))
sound-*β‚š' r (p *X+ q) xβ‚€ env =
  ⟦ r *β‚š' p βŸ§β‚š (xβ‚€ ∷ env) * xβ‚€ + ⟦ r *β‚š q βŸ§β‚š env β‰‘βŸ¨ apβ‚‚ (Ξ» Ο• ψ β†’ Ο• * xβ‚€ + ψ) (sound-*β‚š' r p xβ‚€ env) (sound-*β‚š r q env) βŸ©β‰‘
  ⟦r⟧ * ⟦p⟧ * xβ‚€ + ⟦r⟧ * ⟦q⟧                     β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• + ⟦r⟧ * ⟦q⟧) (*-associative ⟦r⟧ ⟦p⟧ xβ‚€) βŸ©β‰‘
  ⟦r⟧ * (⟦p⟧ * xβ‚€) + ⟦r⟧ * ⟦q⟧                   β‰‘Λ˜βŸ¨ *-distrib-+l  (⟦p⟧ * xβ‚€) ⟦q⟧ ⟦r⟧ βŸ©β‰‘Λ˜
  ⟦r⟧ * (⟦p⟧ * xβ‚€ + ⟦q⟧)                         ∎
  where
    ⟦r⟧ = ⟦ r βŸ§β‚š env
    ⟦p⟧ = ⟦ p βŸ§β‚š (xβ‚€ ∷ env)
    ⟦q⟧ = ⟦ q βŸ§β‚š env

This concludes phase one of the solver.

Evaluation into polynomialsπŸ”—

Now that we’ve gotten the first phase of the solver done, let’s move on to expressions in the language of natural numbers. Our first move shall be defining expressions in the equational theory of natural numbers.

However, there is an efficiency problem we need to take care of here. If we naively expand out _+_ and _*_ during reflection, we could end up in a situation where we need to contend with a huge amount of suc constructors when large literals get involved. Therefore, we prevent reduction of such functions, but this means this phase of the solver needs to be aware of nat literals.

With that in mind, let’s define our expressions. Note that things that the solver is unable to deal with (for instance, functions that aren’t _+_ or _*_) are represented as variables, and will be replaced during evaluation.

data Expr : Nat β†’ Type where
  ‡0   : βˆ€ {n} β†’ Expr n
  ‡lit : βˆ€ {n} β†’ Nat    β†’ Expr n
  ‡_   : βˆ€ {n} β†’ Fin n  β†’ Expr n
  ‡1+_ : βˆ€ {n} β†’ Expr n β†’ Expr n
  _‡+_ : βˆ€ {n} β†’ Expr n β†’ Expr n β†’ Expr n
  _‡*_ : βˆ€ {n} β†’ Expr n β†’ Expr n β†’ Expr n

We also define an interpretation of expressions into functions

⟦_βŸ§β‚‘ : Expr n β†’ Vec Nat n β†’ Nat
⟦ ‡ i      βŸ§β‚‘ env = lookup env i
⟦ ‡0       βŸ§β‚‘ env = 0
⟦ ‡1+ e    βŸ§β‚‘ env = suc (⟦ e βŸ§β‚‘ env)
⟦ ‡lit k   βŸ§β‚‘ env = k
⟦ e1 ‡+ e2 βŸ§β‚‘ env = ⟦ e1 βŸ§β‚‘ env + ⟦ e2 βŸ§β‚‘ env
⟦ e1 ‡* e2 βŸ§β‚‘ env = ⟦ e1 βŸ§β‚‘ env * ⟦ e2 βŸ§β‚‘ env

We also define an evaluation of expressions into polynomials. The only thing to note here is the evaluation of quoted suc constructors as polynomial addition. This is somewhat inneficient, but in practice we rarely have too many suc constructors to deal with, as we handle literals separately.

↓_ : Expr n β†’ Poly Nat n
↓ (‡ i)      = X[ i ]
↓ ‡0         = 0β‚š
↓ (‡1+ e)    = constβ‚š 1 +β‚š ↓ e
↓ ‡lit k     = constβ‚š k
↓ (e₁ ‡+ eβ‚‚) = (↓ e₁) +β‚š (↓ eβ‚‚)
↓ (e₁ ‡* eβ‚‚) = (↓ e₁) *β‚š (↓ eβ‚‚)

Soundness of evaluationπŸ”—

With all of that machinery in place, our final proof shall be to show that evaluating an expression into a polynomial has the same semantics as the original expression. Luckily, most of the legwork is already done, so we can sit back and enjoy the fruits of our labour.

sound : βˆ€ e β†’ (env : Vec Nat n) β†’ ⟦ ↓ e βŸ§β‚š env ≑ ⟦ e βŸ§β‚‘ env
sound (‡ i) env = sound-X[ i ] env
sound ‡0 env = sound-0β‚š env
sound (‡1+ e) env =
  ⟦ constβ‚š 1 +β‚š (↓ e) βŸ§β‚š env       β‰‘βŸ¨ sound-+β‚š (constβ‚š 1) (↓ e) env βŸ©β‰‘
  ⟦ constβ‚š 1 βŸ§β‚š env + ⟦ ↓ e βŸ§β‚š env β‰‘βŸ¨ ap (Ξ» Ο• β†’ Ο• + ⟦ ↓ e βŸ§β‚š env ) (sound-constβ‚š 1 env) βŸ©β‰‘
  suc (⟦ ↓ e βŸ§β‚š env)               β‰‘βŸ¨ ap suc (sound e env) βŸ©β‰‘
  suc (⟦ e βŸ§β‚‘ env) ∎
sound (‡lit k) env = sound-constβ‚š k env
sound (e₁ ‡+ eβ‚‚) env =
  ⟦ (↓ e₁) +β‚š (↓ eβ‚‚) βŸ§β‚š env     β‰‘βŸ¨ sound-+β‚š (↓ e₁) (↓ eβ‚‚) env βŸ©β‰‘
  ⟦ ↓ e₁ βŸ§β‚š env + ⟦ ↓ eβ‚‚ βŸ§β‚š env β‰‘βŸ¨ apβ‚‚ _+_ (sound e₁ env) (sound eβ‚‚ env) βŸ©β‰‘
  ⟦ e₁ βŸ§β‚‘ env + ⟦ eβ‚‚ βŸ§β‚‘ env     ∎
sound (e₁ ‡* eβ‚‚) env =
  ⟦ (↓ e₁) *β‚š (↓ eβ‚‚) βŸ§β‚š env     β‰‘βŸ¨ sound-*β‚š (↓ e₁) (↓ eβ‚‚) env βŸ©β‰‘
  ⟦ ↓ e₁ βŸ§β‚š env * ⟦ ↓ eβ‚‚ βŸ§β‚š env β‰‘βŸ¨ apβ‚‚ _*_ (sound e₁ env) (sound eβ‚‚ env) βŸ©β‰‘
  ⟦ e₁ βŸ§β‚‘ env * ⟦ eβ‚‚ βŸ§β‚‘ env     ∎

Now, all we need to do is expose an interface for the reflection portion of the solver. The abstract here is VERY IMPORTANT, as it prevents the proof from unfolding into an enormous term that kills our compile times.

abstract
  solve : βˆ€ e₁ eβ‚‚ β†’ (env : Vec Nat n)
        β†’ (⟦ ↓ e₁ βŸ§β‚š env ≑ ⟦ ↓ eβ‚‚ βŸ§β‚š env)
        β†’ ⟦ e₁ βŸ§β‚‘ env    ≑ ⟦ eβ‚‚ βŸ§β‚‘ env
  solve e₁ eβ‚‚ env p = sym (sound e₁ env) Β·Β· p Β·Β· sound eβ‚‚ env

We also expose a function for β€œsimplifying” expressions. In reality this will almost always make the term more complicated, but it’s useful for debugging purposes.

expand : (e : Expr n) β†’ (env : Vec Nat n) β†’ Nat
expand e env = ⟦ ↓ e βŸ§β‚š env

ReflectionπŸ”—

Now, for the truly difficult part: the reflection interface. We begin by defining some pattern synonyms for expressions we want to reflect into our Expr type.

private
  pattern nat-lit n =
    def (quote Number.fromNat) (_ ∷ _ ∷ _ ∷ (lit (nat n)) v∷ _)
  pattern β€œzero” =
    con (quote zero) []
  pattern β€œsuc” x =
    con (quote suc) (x v∷ [])
  pattern _β€œ+”_ x y =
    def (quote _+_) (x v∷ y v∷ _)
  pattern _β€œ*”_ x y =
    def (quote _*_) (x v∷ y v∷ _)

Next, we construct quoted a Expr from a term, replacing any unknown Term nodes with variables. This uses the Variable interface for managing the Fin variables and the environment. A discussion of the internals of this is out of scope of this solver; we have already looked into the abyss too deeply.

private
  build-expr : Variables Nat β†’ Term β†’ TC (Term Γ— Variables Nat)
  build-expr vs (nat-lit n) = do
    ‡n ← quoteTC n
    pure $ con (quote ‡lit) (‡n v∷ []) , vs
  build-expr vs β€œzero” = do
    pure $ con (quote ‡0) (unknown h∷ []) , vs
  build-expr vs (β€œsuc” t) = do
    e , vs ← build-expr vs t
    pure $ con (quote ‡1+_) (unknown h∷ e v∷ []) , vs
  build-expr vs (t₁ β€œ+” tβ‚‚) = do
    e₁ , vs ← build-expr vs t₁
    eβ‚‚ , vs ← build-expr vs tβ‚‚
    pure $ con (quote _‡+_) (unknown h∷ e₁ v∷ eβ‚‚ v∷ []) , vs
  build-expr vs (t₁ β€œ*” tβ‚‚) = do
    e₁ , vs ← build-expr vs t₁
    eβ‚‚ , vs ← build-expr vs tβ‚‚
    pure $ con (quote _‡*_) (unknown h∷ e₁ v∷ eβ‚‚ v∷ []) , vs
  build-expr vs tm = do
    (v , vs') ← bind-var vs tm
    pure $ con (quote ‡_) (v v∷ []) , vs'

Next, let’s define the quoted forms of some terms that we will use to interface with the solver.

private
  β€œexpand” : Term β†’ Term β†’ Term
  β€œexpand” e env = def (quote expand) (unknown h∷ e v∷ env v∷ [])

  β€œsolve” : Term β†’ Term β†’ Term β†’ Term
  β€œsolve” lhs rhs env =
    def (quote solve)
        (unknown h∷ lhs v∷ rhs v∷ env v∷ def (quote refl) [] v∷ [])

The actual macrosπŸ”—

Now, the actual reflection API calls. In order to keep drawing this file out, we start by defining some useful debugging macros. As we noted a looong time ago, we don’t want to unfold the _+_ or _*_ functions, so let’s make a list of those names so that we can call withReduceDefs more easily.

private
  don't-reduce : List Name
  don't-reduce = quote _+_ ∷ quote _*_ ∷ quote Number.fromNat ∷ []

The repr! macro prints out a bunch of information about a given expression of type Nat. This is very useful when we are debugging.

repr-macro : Nat β†’ Term β†’ TC ⊀
repr-macro n hole =
  withNormalisation false $
  withReduceDefs (false , don't-reduce) $ do
  tm ← quoteTC n
  e , vs ← build-expr empty-vars tm
  size , env ← environment vs
  repr ← normalise $ def (quote ↓_) (size h∷ e v∷ [])
  typeError $ strErr "The expression\n  " ∷
                termErr tm ∷
              strErr "\nIs represented by the expression\n  " ∷
                termErr e ∷
              strErr "\nAnd the polynomial\n  " ∷
                termErr repr ∷
              strErr "\nThe environment is\n  " ∷
                termErr env ∷ []
macro
  repr! : Nat β†’ Term β†’ TC ⊀
  repr! n = repr-macro n

Slightly more useful is the expand! macro. This takes in a natural number, and will fill in the hole with its expanded form. This is intended to be used with the agda2-elaborate-give command in Emacs, which is bound to C-c RET by default.

expand-macro : Nat β†’ Term β†’ TC ⊀
expand-macro n hole =
  withNormalisation false $
  withReduceDefs (false , don't-reduce) $ do
  tm ← quoteTC n
  e , vs ← build-expr empty-vars tm
  size , env ← environment vs
  expanded ← reduce (β€œexpand” e env)
  unify hole expanded

macro
  expand! : Nat β†’ Term β†’ TC ⊀
  expand! n = expand-macro n

Now, finally, we have reached the summit. The nat! macro allows us to automatically solve equations involving natural numbers.

solve-macro : Term β†’ TC ⊀
solve-macro hole =
  withNormalisation false $
  withReduceDefs (false , don't-reduce) $ do
  goal ← infer-type hole >>= reduce

  just (lhs , rhs) ← get-boundary goal
    where nothing β†’ typeError $ strErr "Can't determine boundary: " ∷
                                termErr goal ∷ []
  elhs , vs ← build-expr empty-vars lhs
  erhs , vs ← build-expr vs rhs
  size , env ← environment vs
  (noConstraints $ unify hole (β€œsolve” elhs erhs env)) <|> do
    nf-lhs ← normalise (β€œexpand” elhs env)
    nf-rhs ← normalise (β€œexpand” erhs env)
    typeError (strErr "Could not solve the following goal:\n  " ∷
                 termErr lhs ∷ strErr " ≑ " ∷ termErr rhs ∷
               strErr "\nComputed normal forms:\n  LHS: " ∷
                 termErr nf-lhs ∷
               strErr "\n  RHS: " ∷
                 termErr nf-rhs ∷ [])

macro
  nat! : Term β†’ TC ⊀
  nat! = solve-macro

ExamplesπŸ”—

Congratulations! We now have a solver. Let’s marvel at all of our hard work for a moment.

private
  wow-good-job : βˆ€ x y z
               β†’ (x + 5 + suc y) * z ≑ z * 5 + x * z + z + z * y
  wow-good-job x y z = nat!

Thus concludes our journey. There is still room for improvement, however. A sparse representation would be much more efficient, but these proofs are already quite difficult to begin with. For the brave, here is what a sparse representation might look like.

private
  data SparsePoly {a} (A : Type a) : Nat β†’ Type a where
    const-sparse : βˆ€ {n} β†’ A β†’ SparsePoly A n
    shift        : βˆ€ {n} β†’ (j : Nat) β†’ SparsePoly A n
                   β†’ SparsePoly A (j + n)
    _*X^_+_      : βˆ€ {n} β†’ SparsePoly A (suc n) β†’ Nat β†’ SparsePoly A n
                   β†’ SparsePoly A (suc n)