Integer¶
Integer keeps whole-number arithmetic exact as values grow from a native
word to many limbs. Compact handles and shared kernels support both scalar
and batch work. The reference describes public calls.
Representation¶
On the qualified target, an Integer is a 16-byte handle with one of four representations:
| Variant | Purpose and Invariants |
|---|---|
| Inline signed 64-bit | Stores values within \([-2^{63}, 2^{63}-1]\), including zero. |
| Inline unsigned 64-bit | Stores wide unsigned values within \([2^{63}, 2^{64}-1]\) without heap allocation. |
| Static literal words | References compile-time arbitrary-precision integer literals. |
| Shared sign-magnitude | Heap-allocated 32-bit limbs for dynamic runtime values exceeding 64 bits. |
Equality, comparisons, hashing, text, and JSON all depend on the mathematical value, regardless of how it is stored. Zero has one canonical form and no negative sign. When a value shrinks to 64 bits, it returns to inline storage.
Heap magnitudes use one malloc block: an atomic reference-count header
followed by 32-bit limbs. Copies share the block until an update needs its
own storage, so changing an Integer leaves existing copies and dictionary
keys intact. To build a large integer, the caller allocates the required
space, fills its words, and exposes the handle only when it is complete.
Kernels write every word they cover; only skipped words need clearing.
System allocations use std.ffi.external_call. Adopted Mojo List buffers
must instead be freed by the native runtime allocator, so the two sources
are tracked separately. Every allocation has a checked size, though physical
out-of-memory remains fatal.
Keeping the handle at 16 bytes lets scalar operations pass a compact value. View metadata belongs to the batch container, where it does not enlarge every scalar argument and result.
Heap magnitudes keep 32-bit words, but the kernels that stream through them do
64-bit work. Addition, subtraction, shifts, comparison, and the conversion to and
from the 64-bit limbs of division and square roots load or store two adjacent
words at once (alignment 4, so any word offset is valid), carrying through 64-bit
arithmetic; multiplication multiplies word pairs as 64-bit limbs. The in-place
updates (+=, <<=) share these kernels. Pairing the words uses the target's
64-bit arithmetic while preserving the existing storage representation.
Arithmetic¶
Addition, subtraction, and comparison use native paths for small values and
word kernels for larger magnitudes. Comparisons can stop once a differing
word or magnitude length determines the order.
In-place addition (+=) mutates the underlying buffer directly when the value
uniquely owns its limb allocation and signs are compatible; otherwise, it falls
back to out-of-place addition. In place, the shared addition kernel writes the
sum over the left operand's own words, which is safe because it reads each pair
of words before writing it; the carry word goes into spare capacity, and the
words move to a larger allocation only when there is none.
-
Bitwise operations (
&,|,^,~): Follow Python's semantics on infinite two's complement. One kernel reads both magnitudes two words at a time, converts a negative operand on the fly (\(-m\) is \(\lnot m + 1\), extended with ones past its words), combines the pairs, and converts a negative result back the same way. The result has one word more than the longer operand, which holds the sign extension. Batch updates in place use the same kernel. -
Multiplication: Inspects operand magnitudes and dispatches to specialized kernels based on word width: native limb multiplication for small values, a dedicated squaring kernel for identical operands, Karatsuba multiplication for medium-sized integers, and blocked multiplication for operands of mismatched lengths. Multiplication kernels never allocate dynamically; callers prepare result and scratch buffers before entering limb loops.
- Division (
//,%,div_rem_*): Implements Knuth's Algorithm D over 64-bit limbs. A single scratch buffer (allocated on the stack for inputs up to 80 limbs) holds normalized operands with the divisor's leading bit set. Each quotient digit is approximated from the two leading limbs using a precomputed reciprocal (requiring two multiplications instead of hardware division, based on Möller and Granlund, Improved division by invariant integers, 2011), and corrected by the third limb to ensure error never exceeds one unit. The quotient and remainder are written directly into target allocations. The floating-point single-rounding core uses the same division engine. A divisor of at most 64 bits needs no scratch block: one pass from the dividend's top pair shifts each pair by the divisor's normalization as it reads it, divides it with the same reciprocal, and writes the quotient straight into its allocation; the remainder is a native value.%, and sogcd, skips the quotient. A quotient that floor division (operands of opposite signs) or Euclidean division (a negative dividend) must round away from zero gains its one in its words before it becomes an Integer, and the remainder becomes the divisor less the truncated one. - Greatest Common Divisor (
gcd): Magnitudes of up to two 64-bit limbs take Stein's binary gcd, after one division when the larger has at least 16 more bits: Stein's steps, a shift and a subtraction each, cost less than Euclid's divisions, but take about one step per bit by which the larger operand exceeds the smaller. Each step takes its shift from the wrapped difference, whose trailing zeros are its magnitude's, so counting them overlaps the rest of the step. While either operand has two limbs, a step forms both differences and picks one by the operands' order without a branch (Stein's step; Knuth, The Art of Computer Programming, vol. 2, §4.5.2, Algorithm B), and counts the low limb's trailing zeros alone; then one-limb steps finish. Longer operands take Lehmer's algorithm on 64-bit limbs in one scratch block. Each step reduces the leading 128 bits of both operands, taken at the same bit position, by Euclid's algorithm into a \(2 \times 2\) matrix of determinant 1 with entries below \(2^{64}\): the double-digit step of Möller (On Schönhage's algorithm and subquadratic integer gcd computation, Mathematics of Computation 77, 2008). It stops while the remainders' high limbs are still at least 2, so its quotients are those of the full operands, and applying the matrix's inverse, in one pass over the limbs, leaves both nonnegative and about 62 bits shorter. A step that cannot proceed (a quotient past 64 bits, or nearly equal operands) takes one division, as do operands more than a limb apart; once both fit two limbs, the binary gcd finishes. Rational normalization relies directly on this kernel. - Factorials (
factorial,factorial2): \(n!\) for \(n \le 20\) and \(n!!\) for \(n \le 33\), the values that fit anInt64, come from tables. Beyond, \(n!\) is its odd part times \(2^{n - \operatorname{popcount}(n)}\) (Legendre's formula for the power of two), and the odd part is the product over \(a \ge 0\) of the odd numbers up to \(\lfloor n / 2^a \rfloor\), each odd \(j\) coming in once for every \(a\) with \(j 2^a \le n\). An odd double factorial is one such level, and \((2m)!! = 2^m m!\). The odd numbers pack into 64-bit words in fixed groups: in a level whose terms have at most \(b\) bits, any \(\lfloor 64 / b \rfloor\) of them fit a word, so no product is checked. The words multiply on limbs in one scratch block, into one accumulator a limb at a time up to 96 words and in halves beyond, so that products past the Karatsuba threshold meet at the top. The power of two is a shift as the result's words are stored, in one allocation. - Square roots (
isqrt): Zimmermann's Karatsuba square root (Brent and Zimmermann, Modern Computer Arithmetic, Algorithm 1.12) returns the root and its remainder together. Write \(a = (a_3 b + a_2) b^2 + a_1 b + a_0\) with \(a_3 \ge b / 4\). With \((s', r')\) the root and remainder of \(a_3 b + a_2\), and \((q, u)\) the quotient and remainder of \((r' b + a_1) / (2 s')\), the root \(s = s' b + q\) has the remainder \(u b + a_0 - q^2\). Since \(s' \ge b / 2\), \(q \le b\) and \(q^2 \le 2s - 1\), so a negative remainder needs one correction: \(s - 1\), with \(r + 2s - 1\). - Up to 256 bits the step runs in native integers, with \(b = 2^k\) and
\(k = \lfloor(\text{bits} + 1) / 4\rfloor\) between 32 and 64, over a 128-bit
root from a
Float64estimate and one Newton step. The top part has at most 128 bits, so \(s' < 2^{64}\) and \(r' b + a_1 < 2^{129}\); its quotient by \(2s'\) is that of \((r' b + a_1) \gg 1\) by \(s'\), a 128-by-64-bit division, and the dropped bit returns in the remainder. - Beyond, the step runs on 64-bit limbs (Zimmermann, Karatsuba Square Root, INRIA Research Report 3805, 1999), in one scratch block of \(3.5n + 1\) limbs for an \(n\)-limb root (on the stack up to \(n = 45\)). The radicand is shifted left by \(2k\) bits so that its top limb is at least \(2^{62}\). The top half's root is then \(s' \ge B^h / 2\), already a normalized divisor for the in-place limb division, which divides by \(s'\) and halves the quotient: an odd quotient adds \(s'\) to the remainder. \(q^2\) goes to limbs the division has freed. When \(s'\) is all ones and \(q = B^l\), the uncorrected root \(B^n\) overflows its limbs; the correction then adds \(2B^n\) to the remainder and brings the root back within them. With \(4^k a = S^2 + R\) and \(s_0 = S \bmod 2^k\), the root is \(S \gg k\) and the remainder \((R + 2 S s_0 - s_0^2) / 4^k\).
- Roots (
iroot): Degree 2 takes the square root above. Higher degrees take Zimmermann's root (Brent and Zimmermann, Modern Computer Arithmetic, Cambridge University Press, 2010, chapter 1), which finds the root's bits from the top. With \(S\) the floor \(k\)-th root of a prefix \(P\) of the radicand, and \(R\) the remainder \(P - S^k\) followed by the radicand's next \(b\) bits, one Newton step gives the root of the longer prefix as \(S 2^b + q\), for \(q = \lfloor R / (k S^{k-1}) \rfloor\) clamped below \(2^b\). Each level adds at most \((x + \log_2 k) / 2\) bits to reach an \(x\)-bit root, about doubling it, and the result is then at most one too large. - Every level's root is certified exactly before the next step: from above
while \(S^k > P\), and from below through \((S + 1)^k - S^k > k S^{k-1}\), which
the remainder settles unless it exceeds \(k S^{k-1}\). For small roots of
high degrees, where that bound is weak, a
Float64comparison of \(P / S^k\) with \((1 + 1/S)^k\) settles it, with a margin of \((k + 64) 2^{-50}\) above the worst rounding of both sides (correctly rounded operations err by at most \(2^{-53}\) each). Only when neither does, and not after a step down, is \((S + 1)^k\) computed. Exactness depends neither on the book's bound nor on the seed. - The seed is a
Float64estimate:exp2andlog2of the leading 64 bits, with the exponent split so that the root taken stays below \(2^{1 + 64/k}\), then one Newton step inFloat64for roots past 28 bits. Mojo'sexp2andlog2are good to about \(2^{-31}\) relatively (measured), the refined estimate to about \(2^{-50}\). - Radicands of up to 256 bits never reach the levels. Native Newton steps on
the exact residual, \(\lfloor (P - S^k) / (k S^{k-1}) \rfloor\), take the
estimate to the root's unit in a step or two. Products are checked for
overflow by bit lengths, splitting a factor at half the width when the
lengths sum to one past it. A power past \(2^{256}\) steps down by
\(S (1 - P / S^k) / k\) from
Float64values, or by at least \(S \gg 52\): just below the root of \(2^{256} - 1\) theFloat64step is noise. Up to 64 bits every power fits 128 bits, unchecked, and roots of at most 11 bits take their bits from the top instead, one power each, chosen by selects rather than branches: a chain ofFloat64operations takes longer. - Longer radicands start from a native seed: up to \(\lfloor 256/k \rfloor\) bits for degrees up to 8, the largest whose prefix fits 256 bits, and 30 bits, within a unit of the estimate, for higher degrees, certified on limbs. The levels then run on 64-bit limbs in one scratch block (on the stack for cube roots of up to 2,880 bits), with the word kernels' products and the limb division.
- Exact scaled addition (
axpy): Computes \(a \cdot x_i + y_i\) exactly across all elements of vector batches, pairing elements by logical index across any stride or slice selection. The scalar coefficient broadcasts across elements, and execution routes through the unified batch driver.
Number theory¶
Number-theory functions support exact simplification, such as removing square
factors from a radical, and primality testing. The calculations are exact;
is_prime above \(2^{64}\) gives a probable-prime answer, as described below.
- Factor removal (
remove_factor): Divides by \(p, p^2, p^4, \ldots\) while each power divides, then back down through the same powers, so a multiplicity \(k\) costs about \(2 \log_2 k\) divisions instead of \(k\). Factors of two come from the count of trailing zero bits. - Trial division (
trial_division): Tries 2, 3, 5 and then the numbers coprime to 30, eight in every thirty, instead of keeping a table of primes. A composite candidate never divides, because its prime factors were divided out before it, so the result equals division by primes alone. Remainders by a one-word candidate run limb by limb without allocating, and once the rest fits in 64 bits the search continues on native integers. It stops when a candidate's square exceeds the rest, which is then 1 or a prime; that prime is reported as a factor when it is below the bound. - Exact roots (
iroot_exact): Cheap tests reject most inputs before a root is taken. The trailing zero count must be a multiple of the degree. A square must be a quadratic residue modulo 64, 63, 65 and 11, which together pass fewer than 1% of non-squares. For another degree \(d\), the two smallest primes \(\ell = kd + 1\) serve as witnesses: an \(x\) coprime to \(\ell\) is a \(d\)-th power modulo \(\ell\) only if \(x^{(\ell - 1)/d} \equiv 1 \pmod{\ell}\). A survivor takes the floor root with its remainder (Zimmermann's square root for squares, the root kernel above otherwise): a zero remainder is the exact root. - Perfect powers (
perfect_power): Tries each prime exponent up to the bit length, odd ones only for a negative input. After a root is found, the search continues on the root with the same prime, so composite exponents build up as products. - Primality (
is_prime): Below \(2^{64}\), trial division by the primes up to 37 and then Miller–Rabin with the first \(k\) prime bases, stopping once \(n\) is below \(\psi_k\), the smallest composite that passes those bases (Jaeschke 1993; Sorenson and Webster 2017). Twelve bases cover every 64-bit \(n\), since \(\psi_{12} \approx 3.2 \times 10^{23}\), so these answers are proven. Above \(2^{64}\) the test is Baillie–PSW: trial division by the primes below 1000, a strong probable-prime test to base 2, a perfect-square check (without which the parameter search below would not end), and a strong Lucas test with Selfridge's parameters \(P = 1\), \(Q = (1 - D)/4\) for the first \(D\) in \(5, -7, 9, -11, \ldots\) with Jacobi symbol \(-1\). No composite is known to pass Baillie–PSW, but that is not a proof. - Jacobi symbol (
jacobi): The binary algorithm with quadratic reciprocity, on native words once both operands fit in 64 bits. - Rational gcd, lcm and roots:
rational.gcdis the gcd of the numerators over the lcm of the denominators. This is already in lowest terms, because a prime that divides both numerators divides neither denominator;lcmis the dual. A fraction in lowest terms is a perfect power exactly when its numerator and denominator are, which is howroot_exactdecides.
Through vmap, iroot_exact and root_exact give two results: the roots,
with 0 where there is none, and a Mask of where there is one (see
Batches and execution). trial_division returns a
TrialDivision, which a batch cannot hold, so it applies to one value at a time.
The package-level gcd and lcm remain the Integer versions, so vmap[gcd]
keeps working; the Rational versions are apn_mojo.rational.gcd and
apn_mojo.rational.lcm.
The suite tests/test_number_theory.mojo checks these functions against
tests/fixtures/number_theory.txt, which tests/fixtures/number_theory.py
writes with gmpy2. The fixture holds the 162 strong pseudoprimes to base 2
below \(10^7\), the 43 Carmichael numbers below \(10^6\), the 58 strong Lucas
pseudoprimes below \(10^6\), the Mersenne prime exponents below 1300, 27 random
primes of 64 to 4096 bits with their odd neighbours, and Jacobi symbols of
multiword values. The suite also compares is_prime with a sieve for every
\(n < 10^6\), and Baillie–PSW alone for every odd \(n < 10^5\), since is_prime
never reaches it below \(2^{64}\).
Hashing and dictionary keys¶
Integer implements Mojo's Hashable trait for use in Dict and Set.
Hashing reads the canonical sign, length, and limb words without allocating.
Equal values have equal hashes, regardless of representation. These hashes
are process-local and are unsuitable for persistent storage or cryptography.
For a hash that is the same in every process and release, use stable_hash
(see Float).
Measured performance¶
The benchmark results summarize Integer arithmetic and number theory, with a full report for individual operations and sizes. Compare like workloads: division by a small word and GCD of two wide operands exercise different paths.