34.3. Prime splitting via Kummer-Dedekind
34.3.1. The story
In the Gaussian integers ℤ[i], the prime 5 stops being prime: it
factors as (2 + i)(2 - i). The prime 3 stays prime. The prime 2
does something stranger, becoming a unit times the square of 1 + i.
Every prime of ℤ meets one of these three fates in ℤ[i], and which
one is decided by a finite computation: factor x² + 1 modulo
p. Two distinct roots mod 5, no roots mod 3, a repeated root
mod 2.
That is not a coincidence about ℤ[i]; it is the first case of the
Kummer-Dedekind theorem, the standard dictionary between factoring a
polynomial over 𝔽ₚ and factoring the ideal (p) in the ring of
integers of a number field. It is also exactly the computation the hex
factorization pipeline automates. This page takes two concrete number
fields, factors their defining polynomials modulo a handful of primes
with HexPolyFp and
HexBerlekamp, and reads off the arithmetic of
primes upstairs, with
HexBerlekampZassenhaus certifying
the inputs and several of the answers. The dictionary itself is quoted
number theory, stated carefully below but not re-proved here; the
closing section is precise about that boundary.
34.3.2. Two number fields, certified
A number field is K = ℚ(α) for α a root of an irreducible integer
polynomial. This page works with two of them: ℚ(i), defined by
x² + 1, and the cubic field ℚ(∛2), defined by x³ - 2. Both
polynomials are Hex.ZPoly values, dense integer polynomials
with coefficients listed from the constant term up.
Irreducibility is the entry ticket: it is what makes ℚ(x)/(f) a
field of degree deg f rather than a product of smaller pieces. The
executable check is Hex.ZPoly.isIrreducible, which runs the
full Berlekamp-Zassenhaus factorizer Hex.ZPoly.factorize and
inspects the result; the irreducibility elaborator then produces the
kernel-checked theorem, exactly as in
the AES modulus tutorial but over ℤ.
open Hex
namespace PrimeSplittingTutorial
/-- `x² + 1`, the defining polynomial of `ℚ(i)`. -/
def fGauss : ZPoly := #p[1, 0, 1]
/-- `x³ - 2`, the defining polynomial of `ℚ(∛2)`. -/
def fCubic : ZPoly := #p[-2, 0, 0, 1]
-- Both pass the executable irreducibility check ...
#guard ZPoly.isIrreducible fGauss
#guard ZPoly.isIrreducible fCubic
-- ... because each is its own entire factorization.
#guard (ZPoly.factorize fGauss).factors
= #[(fGauss, 1)]
#guard (ZPoly.factorize fGauss).scalar = 1
#guard (ZPoly.factorize fCubic).factors
= #[(fCubic, 1)]
/-- Kernel-certified: `x² + 1` is irreducible. -/
theorem fGauss_irred : ZPoly.Irreducible fGauss :=
irreducibility fGauss
/-- Kernel-certified: `x³ - 2` is irreducible. -/
theorem fCubic_irred : ZPoly.Irreducible fCubic :=
irreducibility fCubic
end PrimeSplittingTutorial
The certified theorems are worth a remark each. For x³ - 2 the
elaborator finds an Eisenstein-style witness at the prime 2; for
x² + 1, which is Eisenstein at no prime, it finds a prime p where
the polynomial stays irreducible mod p and emits a single-prime
modular certificate. Both proof terms are replayed by the kernel on
literal data.
34.3.3. The correspondence
Fix a number field K = ℚ(α) with α an algebraic integer, f its
monic minimal polynomial of degree n, and let 𝒪 be the ring of
integers of K. In 𝒪 a prime p of ℤ need not stay prime, but
unique factorization is restored at the level of ideals: the ideal
(p) factors uniquely as
(p) = P₁^e₁ · P₂^e₂ ⋯ P_r^e_r
with distinct prime ideals Pᵢ. Each Pᵢ has a residue degree
fᵢ, meaning its residue ring 𝒪/Pᵢ is the finite field of p^fᵢ
elements, and the exponent eᵢ is its ramification index. The
invariants balance: Σ eᵢ·fᵢ = n, whatever p does.
The Kummer-Dedekind theorem computes all of it from one polynomial
factorization. Suppose p does not divide the conductor of ℤ[α]
(the caveat gets its own section below; for this page's two fields the
conductor is 1 and the hypothesis is vacuous). Factor the reduction
of f into monic irreducibles over 𝔽ₚ:
f ≡ g₁^e₁ · g₂^e₂ ⋯ g_r^e_r (mod p)
Then the primes of 𝒪 above p are exactly Pᵢ = (p, gᵢ(α)), one
per distinct factor, with residue degree fᵢ = deg gᵢ and
ramification index the multiplicity eᵢ. The three fates of the
opening section get their standard names from the shape of this data:
p splits completely when r = n (all factors linear and
distinct), is inert when f stays irreducible (r = 1, e = 1),
and ramifies when some eᵢ > 1, which for p prime to the
conductor happens exactly when p divides the discriminant of f.
34.3.4. The mod-p pipeline
The right-hand side of the dictionary is a computation hex already
has, in three moves. Hex.ZPoly.modP reduces the integer
polynomial to an Hex.FpPoly.
Hex.FpPoly.squareFreeDecomposition (Yun's algorithm) separates
the multiplicities, which matters precisely at the ramified primes.
Hex.Berlekamp.berlekampFactor then splits each square-free
part into irreducibles, as described in the
HexBerlekamp chapter. Composing the three and
keeping only the shape (deg gᵢ, eᵢ) gives the splitting type as a
sorted list of (residue degree, ramification index) pairs.
open Hex
namespace PrimeSplittingTutorial
/-- The shape of `f mod p`: one
`(degree, multiplicity)` pair per distinct irreducible
factor, sorted. Under Kummer-Dedekind, each pair is a
prime above `p` with that residue degree and
ramification index. -/
def splittingType (p : Nat) [ZMod64.Bounds p]
[ZMod64.PrimeModulus p] (f : FpPoly p) :
List (Nat × Nat) :=
let dec := FpPoly.squareFreeDecomposition
ZMod64.PrimeModulus.prime f
let pairs := dec.factors.flatMap fun sf =>
if h : DensePoly.leadingCoeff sf.factor = 1 then
(Berlekamp.berlekampFactor sf.factor
(p:ℕinst✝¹:ZMod64.Bounds pinst✝:ZMod64.PrimeModulus pf:FpPoly pdec:FpPoly.SquareFreeDecomposition p := FpPoly.squareFreeDecomposition ⋯ fsf:FpPoly.SquareFreeFactor ph:DensePoly.leadingCoeff sf.factor = 1⊢ DensePoly.Monic sf.factor All goals completed! 🐙)).factors.map
fun g => (g.size - 1, sf.multiplicity)
else
[(sf.factor.size - 1, sf.multiplicity)]
pairs.mergeSort fun a b =>
decide (a.1 < b.1) ||
(a.1 == b.1 && decide (a.2 ≤ b.2))
/-- Coefficients as naturals, for readable factors. -/
def coeffNats {p : Nat} [ZMod64.Bounds p]
(f : FpPoly p) : List Nat :=
f.toArray.toList.map ZMod64.toNat
-- The primes this page visits, with machine-word
-- bounds and primality facts as instances.
private instance pm2 : ZMod64.PrimeModulus 2 :=
⟨⊢ Hex.Nat.Prime 2 All goals completed! 🐙⟩
private instance b3 : ZMod64.Bounds 3 :=
⟨⊢ 0 < 3 All goals completed! 🐙, ⊢ 3 < 2 ^ 31 All goals completed! 🐙⟩
private instance pm3 : ZMod64.PrimeModulus 3 :=
⟨⊢ Hex.Nat.Prime 3 All goals completed! 🐙⟩
private instance b5 : ZMod64.Bounds 5 :=
⟨⊢ 0 < 5 All goals completed! 🐙, ⊢ 5 < 2 ^ 31 All goals completed! 🐙⟩
private instance pm5 : ZMod64.PrimeModulus 5 :=
⟨⊢ Hex.Nat.Prime 5 All goals completed! 🐙⟩
private instance b7 : ZMod64.Bounds 7 :=
⟨⊢ 0 < 7 All goals completed! 🐙, ⊢ 7 < 2 ^ 31 All goals completed! 🐙⟩
private instance pm7 : ZMod64.PrimeModulus 7 :=
⟨⊢ Hex.Nat.Prime 7 All goals completed! 🐙⟩
private instance b13 : ZMod64.Bounds 13 :=
⟨⊢ 0 < 13 All goals completed! 🐙, ⊢ 13 < 2 ^ 31 All goals completed! 🐙⟩
private instance pm13 : ZMod64.PrimeModulus 13 :=
⟨⊢ Hex.Nat.Prime 13 All goals completed! 🐙⟩
private instance b31 : ZMod64.Bounds 31 :=
⟨⊢ 0 < 31 All goals completed! 🐙, ⊢ 31 < 2 ^ 31 All goals completed! 🐙⟩
private instance pm31 : ZMod64.PrimeModulus 31 :=
⟨⊢ Hex.Nat.Prime 31 All goals completed! 🐙⟩
end PrimeSplittingTutorial
The if branch is not dead code in general (Berlekamp's precondition
is a monic input), but for the monic polynomials this page reduces it
never fires: every square-free factor Yun returns is monic. Note what
splittingType is: an honest computation, checked below by #guard
against independently known number theory. The kernel-certified
variant of the same factorizations appears in
a later section.
34.3.5. The Gaussian integers
For K = ℚ(i) the ring of integers is ℤ[i] itself, the conductor is
1, and the discriminant is -4, so Kummer-Dedekind applies at every
prime and ramification can only happen at 2.
open Hex
namespace PrimeSplittingTutorial
-- p = 2: x² + 1 ≡ (x + 1)², ramified:
-- (2) = (2, i + 1)² = (1 + i)².
#guard splittingType 2 (ZPoly.modP 2 fGauss)
= [(1, 2)]
-- p = 3: x² + 1 irreducible mod 3, inert: (3) stays
-- prime with residue field of 9 elements.
#guard splittingType 3 (ZPoly.modP 3 fGauss)
= [(2, 1)]
-- p = 5 and p = 13: two distinct roots, split.
#guard splittingType 5 (ZPoly.modP 5 fGauss)
= [(1, 1), (1, 1)]
#guard splittingType 13 (ZPoly.modP 13 fGauss)
= [(1, 1), (1, 1)]
-- Mod 13 the roots are 5 and 8: the factors are
-- x - 5 and x - 8, stored as x + 8 and x + 5.
def g13 : FpPoly 13 := #p[1, 0, 1]
#guard ZPoly.modP 13 fGauss == g13
theorem g13_monic : DensePoly.Monic g13 := ⊢ DensePoly.Monic g13 All goals completed! 🐙
#guard ((Berlekamp.berlekampFactor g13 g13_monic)
|>.factors.map coeffNats) == [[8, 1], [5, 1]]
end PrimeSplittingTutorial
Reading the mod-13 factorization through the dictionary: the primes
above 13 are (13, i - 5) and (13, i - 8), each with residue
degree one. And indeed 13 = (3 + 2i)(3 - 2i) in ℤ[i], with
i ≡ 5 (mod 3 + 2i): the ideal-theoretic answer collapses to honest
element factorizations because ℤ[i] is a principal ideal domain.
The split/inert dichotomy here is a theorem of Fermat in disguise:
x² + 1 has a root mod an odd p exactly when p ≡ 1 (mod 4), so
the primes that split in ℤ[i], equivalently the primes that are sums
of two squares, are exactly those congruent to 1 mod 4. The
#guards above are four instances of that pattern, computed by
Berlekamp factorization rather than quadratic reciprocity.
34.3.6. A cubic field
The field ℚ(∛2) is more interesting: its Galois closure has group
S₃, so primes have five possible fates rather than three, and which
one occurs is no longer a congruence condition on p alone. The ring
of integers is ℤ[∛2] (conductor 1 again), and the discriminant of
x³ - 2 is -108 = -2²·3³, so exactly 2 and 3 ramify.
open Hex
namespace PrimeSplittingTutorial
-- p = 2: x³ - 2 ≡ x³, totally ramified:
-- (2) = (2, ∛2)³ = (∛2)³.
#guard splittingType 2 (ZPoly.modP 2 fCubic)
= [(1, 3)]
-- p = 3: x³ - 2 ≡ (x + 1)³, totally ramified.
#guard splittingType 3 (ZPoly.modP 3 fCubic)
= [(1, 3)]
-- p = 5: one root (3³ = 27 ≡ 2) and an irreducible
-- quadratic: a degree-1 and a degree-2 prime.
#guard splittingType 5 (ZPoly.modP 5 fCubic)
= [(1, 1), (2, 1)]
-- p = 7: no cube root of 2 mod 7, inert.
#guard splittingType 7 (ZPoly.modP 7 fCubic)
= [(3, 1)]
-- p = 31: totally split. 2 ≡ 4³ mod 31, and the
-- roots are 4, 7, and 20.
#guard splittingType 31 (ZPoly.modP 31 fCubic)
= [(1, 1), (1, 1), (1, 1)]
def g31 : FpPoly 31 := #p[29, 0, 0, 1]
#guard ZPoly.modP 31 fCubic == g31
theorem g31_monic : DensePoly.Monic g31 := ⊢ DensePoly.Monic g31 All goals completed! 🐙
-- The factors x - 4, x - 7, x - 20, stored as
-- x + 27, x + 24, x + 11.
#guard ((Berlekamp.berlekampFactor g31 g31_monic)
|>.factors.map coeffNats)
== [[27, 1], [24, 1], [11, 1]]
-- Whatever the shape, residue degrees weighted by
-- ramification indices sum to the field degree.
#guard ((splittingType 5 (ZPoly.modP 5 fCubic)).foldl
(fun s df => s + df.1 * df.2) 0) = 3
end PrimeSplittingTutorial
The unramified shapes follow the cubic-residue arithmetic of 2. For
p ≡ 2 (mod 3) cubing is a bijection on 𝔽ₚ, so x³ - 2 has
exactly one root and the shape is always a line times a conic, as at
p = 5. For p ≡ 1 (mod 3) the cubes form an index-three subgroup:
if 2 lands in it the polynomial splits completely, as at p = 31;
if not there are no roots at all and p is inert, as at p = 7.
There is a deeper pattern behind which shape occurs how often. The
splitting type of an unramified p is the cycle type of its Frobenius
element in the Galois group S₃, and the Chebotarev density theorem
says each conjugacy class is hit with frequency proportional to its
size: totally split with density 1/6, the line-times-conic shape
with density 1/2, inert with density 1/3. The five primes above
are a small sample; the density statement is what the sample is drawn
from.
34.3.7. Certified splittings
splittingType is compiled code checked by #guard. For a
kernel-checked account of the same factorizations, the factor_poly
and irreducibility elaborators from
HexBerlekamp emit certificate-backed proof
terms over FpPoly p, as described in
the factor tactics chapter.
open Hex
namespace PrimeSplittingTutorial
/-- A certified factorization of `x² + 1` mod 13: the
`factors_mul` and `factors_irred` fields are proofs,
replayed by the kernel from Rabin certificates. -/
noncomputable def fac13 := factor_poly g13
example : fac13.factors = [#p[8, 1], #p[5, 1]] := ⊢ fac13.factors = [DensePoly.ofCoeffs #[8, 1], DensePoly.ofCoeffs #[5, 1]]
All goals completed! 🐙
example : fac13.scalar = 1 := rfl
def g7 : FpPoly 7 := #p[5, 0, 0, 1]
#guard ZPoly.modP 7 fCubic == g7
/-- Kernel-certified inertness: `x³ - 2` stays
irreducible mod 7. -/
theorem g7_irred : FpPoly.Irreducible g7 :=
irreducibility g7
end PrimeSplittingTutorial
A Hex.FpPoly.Factored value is not a list of factors that some
compiled routine printed: its factors_mul field proves the product
reconstructs g13 and its factors_irred field proves each listed
factor irreducible, so the mod-13 splitting data above 13 is backed
by a kernel-checked factorization. Likewise g7_irred is precisely
the "inert" claim for 7 in ℚ(∛2), in certified form.
34.3.8. The conductor caveat
Kummer-Dedekind's hypothesis was stated above and now has to be taken
seriously: the dictionary reads factorizations of f mod p correctly
only when p does not divide the conductor of ℤ[α] in 𝒪, the
largest ideal of 𝒪 contained in ℤ[α]. A prime dividing the
conductor also divides the index (𝒪 : ℤ[α]), and the discriminants
keep the books: disc f = (𝒪 : ℤ[α])² · disc K. When ℤ[α] is all
of 𝒪, as for ℤ[i] and ℤ[∛2], the conductor is 1 and every
prime is safe, which is why the sections above could proceed without
comment.
The caveat is not hypothetical, and Dedekind found the smallest
counterexample: K = ℚ(θ) for θ a root of x³ - x² - 2x - 8. Here
disc f = -2012 = 2²·(-503) while disc K = -503, so the index is
2, and at p = 2 the dictionary misreads:
open Hex
namespace PrimeSplittingTutorial
/-- Dedekind's cubic `x³ - x² - 2x - 8`. -/
def fDedekind : ZPoly := #p[-8, -2, -1, 1]
#guard ZPoly.isIrreducible fDedekind
-- Mod 2 the polynomial is x²·(x + 1), which would
-- read as one ramified prime and one unramified one.
#guard splittingType 2 (ZPoly.modP 2 fDedekind)
= [(1, 1), (1, 2)]
end PrimeSplittingTutorial
The computed factorization is perfectly correct as a factorization
mod 2; what fails is the dictionary. The true splitting, computed
with the full ring of integers, is that 2 splits completely: three
distinct primes of residue degree one, no ramification at all
(consistent with 2 not dividing disc K = -503). No better choice
of θ fixes it, and the obstruction is charmingly finite: three
distinct degree-one primes would require three distinct monic linear
polynomials over 𝔽₂, and there are only two. So every generator
of this field has even index, 2 divides every conductor, and the
polynomial dictionary is silent at 2 no matter which defining
polynomial one factors. Primes like this are called common index
divisors, and they are the precise reason the theorem carries its
hypothesis.
34.3.9. What was computed, and what was proved
Three grades of evidence appear on this page, and the point of the page is lost if they blur.
The #guards, including everything splittingType produced, are
computations: evaluated when the manual builds, by the compiled
factorization code a caller would run, and checked by the evaluator
against expected values. They are tests, not theorems.
The irreducibility and factor_poly results (fGauss_irred,
fCubic_irred, fac13, g7_irred) are kernel-checked theorems about
polynomials: the kernel replays a certificate check on literal data,
and library soundness theorems convert the passing check into
irreducibility and product statements. What is certified is the
polynomial factorization, on both sides of the reduction mod p.
The dictionary between those factorizations and ideals, the
Kummer-Dedekind theorem itself, is imported number theory: this page
states it and instantiates it in prose, and nothing here formalizes
the ideal-theoretic side. That theorem does exist in formalized form,
as Mathlib's
KummerDedekind.normalizedFactorsMapEquivNormalizedFactorsMinPolyMk,
a bijection between the primes above p and the irreducible factors
of the reduced minimal polynomial, conductor hypothesis and all.
Connecting hex's executable factor lists to that statement, through
the correspondence layers described in
the HexBerlekampZassenhaus chapter,
is exactly the kind of bridge the *Mathlib companion libraries
exist for; this page stops at the polynomial boundary and says so.
34.3.10. Cross-references
-
HexBerlekampZassenhausdocumentsHex.ZPoly.factorize, the integer factorizer whose irreducibility checks anchor this page's number fields. -
HexBerlekampis the mod-pfactorization engine: Berlekamp's algorithm, Rabin's test, and the certificate checkers behindfactor_polyandirreducibility. -
HexPolyFpprovides the prime-field polynomials, including the square-free decomposition that detects ramification. -
The factor tactics chapter documents the elaborators used in the certified section, including their Mathlib-facing forms.
-
Why the AES modulus works is the same certificate story at a single prime, told slowly.