Pell solutions beyond machine-sized integers

Generate large exact numbers, then check the equation they must satisfy.

program · output · proof · check · playground


The question

Pell’s equation asks for whole numbers x and y with

x² − 2y² = 1.

(3, 2) is a solution, since 9 − 8 = 1. There are infinitely many more, and they grow quickly. Starting from (1, 0), each step turns a solution into the next one with a fixed recipe.

What are the solutions at steps 0, 1, 5, 8 and 50, and does each one really satisfy the equation? At step 50 the numbers have 38 digits.


What we tell eyekind

fact(requested_step(0), requested_step(1), requested_step(5), requested_step(8), requested_step(50))
# pell(N, X, Y): the solution reached after N steps.
fact(pell(0, 1, 0))
implied_by(pell(N, X, Y), [
  gt(N, 0), is_(Before, sub(N, 1)), pell(Before, X0, Y0),
  is_(X, add(mul(3, X0), mul(4, Y0))), is_(Y, add(mul(2, X0), mul(3, Y0))),
])
implies([requested_step(N), pell(N, X, Y)], solution(N, X, Y))
implies([solution(N, X, Y), is_(1, sub(mul(X, X), mul(2, mul(Y, Y))))], equation_holds(N))
query(solution(N, X, Y))
query(equation_holds(N))

Start with (x, y) = (1, 0). Each step uses the previous pair:

These are the coefficients obtained by multiplying x + y√2 by 3 + 2√2.


What eyekind concludes

// Answers: each instance of a query() that holds.
solution(0, 1, 0)
solution(1, 3, 2)
solution(5, 3363, 2378)
solution(8, 665857, 470832)
solution(50, 94741125149636933417873079920900017937, 66992092050551637663438906713182313772)
equation_holds(0)
equation_holds(1)
equation_holds(5)
equation_holds(8)
equation_holds(50)

The solutions grow by a factor of almost six per step. At step 50:

x = 94741125149636933417873079920900017937
y = 66992092050551637663438906713182313772

The program also reports equation_holds(50): x² − 2y² is exactly 1.


Why exact arithmetic matters

The squares are enormous (76 digits) but differ by exactly one. Rounding them to floating-point numbers, which hold only about 16 digits exactly, would lose the information needed for that subtraction. eyekind’s integers are exact at any size, so the multiplication and subtraction remain exact throughout.


The proof, line by line

eyekind --proof writes down how it reached each answer. The full proof has 271 steps and is 41 KB (read it all). Below are its claims, all its clauses, the complete chains for steps 0 and 1 and their equation checks, and the equation check at step 50, verbatim:

// Answers: each instance of a query() that holds.
solution(0, 1, 0)
solution(1, 3, 2)
solution(5, 3363, 2378)
solution(8, 665857, 470832)
solution(50, 94741125149636933417873079920900017937, 66992092050551637663438906713182313772)
equation_holds(0)
equation_holds(1)
equation_holds(5)
equation_holds(8)
equation_holds(50)

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(requested_step(0)))
clause(2, fact(requested_step(1)))
clause(3, fact(requested_step(5)))
clause(4, fact(requested_step(8)))
clause(5, fact(requested_step(50)))
clause(6, fact(pell(0, 1, 0)))
clause(7, implied_by(pell(N, X, Y), [gt(N, 0), is(Before, sub(N, 1)), pell(Before, X0, Y0), is(X, add(mul(3, X0), mul(4, Y0))), is(Y, add(mul(2, X0), mul(3, Y0)))]))
clause(8, implies([requested_step(N), pell(N, X, Y)], solution(N, X, Y)))
clause(9, implies([solution(N, X, Y), is(1, sub(mul(X, X), mul(2, mul(Y, Y))))], equation_holds(N)))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(solution(0, 1, 0), clause(8), {N: 0, X: 1, Y: 0}, [requested_step(0), pell(0, 1, 0)])
step(requested_step(0), clause(1), {}, [])
step(pell(0, 1, 0), clause(6), {}, [])
step(solution(1, 3, 2), clause(8), {N: 1, X: 3, Y: 2}, [requested_step(1), pell(1, 3, 2)])
step(requested_step(1), clause(2), {}, [])
step(pell(1, 3, 2), clause(7), {N: 1, X: 3, Y: 2, Before: 0, X0: 1, Y0: 0}, [gt(1, 0), is(0, sub(1, 1)), pell(0, 1, 0), is(3, add(mul(3, 1), mul(4, 0))), is(2, add(mul(2, 1), mul(3, 0)))])
step(gt(1, 0), 'builtin', {}, [])
step(is(0, sub(1, 1)), 'builtin', {}, [])
step(is(3, add(mul(3, 1), mul(4, 0))), 'builtin', {}, [])
step(is(2, add(mul(2, 1), mul(3, 0))), 'builtin', {}, [])
// … steps 11-261: the solutions at steps 5, 8 and 50, and the recurrence down to them
step(equation_holds(0), clause(9), {N: 0, X: 1, Y: 0}, [solution(0, 1, 0), is(1, sub(mul(1, 1), mul(2, mul(0, 0))))])
step(is(1, sub(mul(1, 1), mul(2, mul(0, 0)))), 'builtin', {}, [])
step(equation_holds(1), clause(9), {N: 1, X: 3, Y: 2}, [solution(1, 3, 2), is(1, sub(mul(3, 3), mul(2, mul(2, 2))))])
step(is(1, sub(mul(3, 3), mul(2, mul(2, 2)))), 'builtin', {}, [])
// … steps 266-269: the equation checks at steps 5 and 8
step(equation_holds(50), clause(9), {N: 50, X: 94741125149636933417873079920900017937, Y: 66992092050551637663438906713182313772}, [solution(50, 94741125149636933417873079920900017937, 66992092050551637663438906713182313772), is(1, sub(mul(94741125149636933417873079920900017937, 94741125149636933417873079920900017937), mul(2, mul(66992092050551637663438906713182313772, 66992092050551637663438906713182313772))))])
step(is(1, sub(mul(94741125149636933417873079920900017937, 94741125149636933417873079920900017937), mul(2, mul(66992092050551637663438906713182313772, 66992092050551637663438906713182313772)))), 'builtin', {}, [])
// 271 steps in all.

It has three parts:

Two kinds of justification occur:

Read the shown steps as sentences:

The steps left out work the same way: each step of the recurrence is one pell step by rule 7 with its four calculations, down to the solution already established. Step 50 builds on step 8, which builds on step 5.


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Checked answers: these follow from the input program.
solution(0, 1, 0)
solution(1, 3, 2)
solution(5, 3363, 2378)
solution(8, 665857, 470832)
solution(50, 94741125149636933417873079920900017937, 66992092050551637663438906713182313772)
equation_holds(0)
equation_holds(1)
equation_holds(5)
equation_holds(8)
equation_holds(50)

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 66)
condition('C2', 'well_founded', 'ok', 271)
condition('C3', 'justification', 'ok', 271)
condition('C4', 'coverage', 'ok', 280)
condition('C5', 're_decision', 'ok', 205)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 281)
condition('C8', 'witness_integrity', 'ok', 0)

// Proof statistics: recorded steps and how they were checked.
steps(271)
verified(66)
recomputed(205)
composed(0)
trusted(0)
claims(10)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics repeat this: 271 steps, 66 verified against the program, 205 recomputed, none taken on trust. The verdict is checked: all five solutions and their equation checks follow from the program.

This checks the five requested instances. It does not establish that every nonnegative solution appears in this sequence, or provide an induction proof for all step numbers.


Try it

eyekind examples/pell-equation.py
eyekind --goal "pell(12, X, Y)" examples/pell-equation.py

The answer is (768398401, 543339720). Add fact(requested_step(12)) to the program to include both its solution and its equation check in the saved run: solution(12, 768398401, 543339720) and equation_holds(12) appear.


Takeaway

Large numbers are easy to get wrong and hard to check by eye. Because every step is exact, the checker can recompute the arithmetic and confirm that the equation really holds.