sysl

Module

sysl.math.bigint

Integers with no width: sign and magnitude, the magnitude a run of 32-bit limbs.

Import import sysl.math.bigint

A general-purpose language without arbitrary precision makes a whole class of program wrong in the same way – a hash reduced modulo something, a factorial, a cryptographic exponent, an identifier that outgrew 64 bits. Go ships math/big and Rust does not, and this takes Go’s side: the alternative is a package that every serious program ends up depending on anyway, and a standard library is where a type everybody has to agree on belongs.

The representation

A BigInt is a sign and a magnitude, and the magnitude is a view of u32 limbs written least significant first with no leading zeros. Zero is the empty magnitude and a clear sign, so there is exactly one representation of it and nothing has to remember whether it is negative.

Limbs are 32 bits and the arithmetic is done in 64. That is what makes every carry, borrow and partial product exact without asking the language for a wider type or a carry flag: a product of two limbs is at most (2^32 - 1)^2, which fits a u64 with room for two more limbs of carry. Sixty-four-bit limbs would halve the work and would need a 128-bit product, which is a different conversation.

What it costs

Every operation that grows allocates, and the docstrings say so where it is not obvious. There is no in-place arithmetic and no reuse of a caller’s buffer: a BigInt is a value that copies, which is what makes a + b * c mean what it looks like, and the cost of that is one allocation per operation. A program doing this in a loop is doing something a big-integer library is not the fastest way to do.

Multiplication is schoolbook and division is Knuth’s algorithm D. Both are quadratic. Karatsuba, Toom-Cook and a Barrett or Montgomery reduction are all improvements that fit behind this same surface, and none of them changes an answer – so they are work somebody does when a program is slow rather than decisions this module has to make now. What this is for is money, identifiers, checksums and the occasional exact computation, none of which is GMP’s territory.

The bit operations, and the reading they settled on

and, or, xor, ~ and the two shifts read a value as a two’s-complement number of infinite width: a non-negative one padded upward with zeros and a negative one padded upward with ones. That is Python’s reading, and it is the only one that is total over both signs without a width the caller would have had to invent. So ~x is -x - 1 for every x, -1 has every bit set, and >> floors toward negative infinity rather than toward zero. bits.sysl builds it and writes the argument out.

There are two divisions and both are here. div_rem truncates, which is what the hardware and the language’s own / do; div_rem_floor rounds toward negative infinity and leaves the remainder with the divisor’s sign, which is what puts a modulus in a fixed range. A language implemented over this type picks one and says which.

What is deliberately not here

Primality. A useful test is Miller-Rabin, and which witness set or how many rounds it should use is a decision about what the caller is doing rather than one this module can make. Nothing here needs one, and a wrong answer from a primality test is the expensive kind.

Index

abs add add_long and bit_length cmp cmp_long div div_floor div_rem div_rem_floor div_rem_long fits_long from_int from_real from_u64 gcd is_even is_odd isqrt lcm mod_floor mod_pow mul mul_long negate not one or parse parse_radix pow rem shl shr sub sub_long test_bit to_real to_string to_string_radix to_u64 trailing_zeros xor zero BigInt Add for BigInt BitAnd for BigInt BitOr for BigInt BitXor for BigInt Display for BigInt Div for BigInt Eq for BigInt From for BigInt From for BigInt Hash for BigInt Mul for BigInt Neg for BigInt Not for BigInt One for BigInt Ord for BigInt Rem for BigInt Shl for BigInt Shr for BigInt Sub for BigInt Zero for BigInt

Functions

abs

abs(x: BigInt) -> BigInt

The magnitude, as a value of this type.

add

add(a: BigInt, b: BigInt) -> BigInt

a + b. Allocates the result’s limbs.

Two values of the same sign add their magnitudes and keep it; two of different signs subtract the smaller magnitude from the larger and take the larger one’s sign, which is where a zero result comes from and is why the sign is dropped when the magnitudes cancel.

add_long

add_long(a: BigInt, n: long) -> BigInt

a + n.

and

and(a: BigInt, b: BigInt) -> BigInt

a & b, on the two’s-complement reading – so (-6) & 3 is 2. Allocates.

The sign follows the operation: a bit of the result is set only where both operands have one, and the sign bit is a bit like any other, so the answer is negative exactly when both operands are.

bit_length

bit_length(x: BigInt) -> usize

How many bits the magnitude occupies, which is zero for zero and is Python’s bit_length.

It answers the same number for x and for -x, the sign not being one of the bits it counts. That is the question a caller actually has – how big is this – and it is the one that makes bit_length(x) <= 64 the test for whether a value still fits a machine word.

cmp

cmp(a: BigInt, b: BigInt) -> int

-1, 0 or 1 as a is below, equal to or above b.

A negative is below every positive, and between two negatives the larger magnitude is the smaller number – which is the one place sign-and-magnitude needs a thought that two’s complement does not.

cmp_long

cmp_long(a: BigInt, n: long) -> int

-1, 0 or 1 as a is below, equal to or above n. Allocates nothing.

That is the whole reason it is here rather than left to cmp(a, from_int(n)). A comparison is what a loop tests and what a sort calls, so it is the operation most often on a hot path and the one where building a value to throw away is least defensible. The signs decide it where they differ, a magnitude of more than two limbs is past anything a long holds, and what is left is two u64s.

div

div(a: BigInt, b: BigInt) -> BigInt

a / b, truncating toward zero.

div_floor

div_floor(a: BigInt, b: BigInt) -> BigInt

a divided by b, rounding toward negative infinity: div_floor(-7, 2) is -4.

div_rem

div_rem(a: BigInt, b: BigInt) -> (BigInt, BigInt)

(quotient, remainder) with the signs applied.

The quotient truncates toward zero and the remainder takes the DIVIDEND’s sign, which is what C, Go, Rust and sysl’s own / and % on machine integers all do, and is what makes a == q * b + r hold. It is not the floored division a mathematician would write, and the difference shows on exactly the cases with a negative operand: -7 / 2 is -3 with a remainder of -1, rather than -4 with a remainder of 1.

Division by zero traps, as it does on a machine integer, rather than answering a Result. That is the language’s own answer for the same mistake and there is no reason for this type to disagree with it – a caller that cannot rule out a zero divisor checks for one.

div_rem_floor

div_rem_floor(a: BigInt, b: BigInt) -> (BigInt, BigInt)

(quotient, remainder) floored: the quotient rounds toward negative infinity and the remainder takes the divisor’s sign. div_rem_floor(-7, 2) is (-4, 1).

This is the other division, and both are here because neither is the right one. div_rem truncates, which is what the hardware does and what makes abs(a / b) independent of the signs; this floors, which is what makes the remainder land in a fixed range – 0..<b for a positive divisor, whatever the dividend was. A program indexing a ring buffer, reducing modulo a modulus, or bucketing a signed coordinate wants the second and gets a negative index from the first.

Python’s // and % are this pair, and a language implemented over this type reaches for it.

The identity a == q * b + r holds for both, which is what makes them two roundings of one division rather than two different operations. They differ only where the remainder is non-zero and the operands’ signs disagree, and there the quotient is one lower and the remainder one divisor higher.

div_rem_long

div_rem_long(a: BigInt, n: long) -> (BigInt, long)

(quotient, remainder) for a machine-sized divisor, the remainder as a long.

The remainder always fits, which is what makes the second result a long rather than another BigInt: it is smaller in magnitude than the divisor, and the divisor was a long. It truncates and takes the dividend’s sign, as div_rem does – div_rem_floor is the other rounding and has no small-operand form, a caller wanting it having div_rem_floor(a, from_int(n)).

A zero divisor traps, as it does everywhere else in this module.

fits_long

fits_long(x: BigInt) -> bool

Whether the value still fits a long, which is the question in front of every to_long and is worth asking on its own when the answer decides a route rather than a value.

from_int

from_int(n: long) -> BigInt

The value n names.

The most negative long is the case worth knowing about: its magnitude cannot be reached by negating it, since that value has no positive counterpart, so the conversion goes through the unsigned reading of the bits.

from_real

from_real(f: real) -> Option[BigInt]

The value f names with its fraction discarded, or None for a NaN or an infinity.

It truncates toward zero, as long(f) does: from_real(-2.7) is -2. A magnitude below one truncates to zero, including a negative one, there being no negative zero here.

Exact for every finite real, however large – which is the reason to convert through this type rather than through a machine integer, where 1e30 has no answer at all.

from_u64

from_u64(n: u64) -> BigInt

The value n names, n being unsigned – so the whole 64-bit range, where from_int stops at the largest positive long.

gcd

gcd(a: BigInt, b: BigInt) -> BigInt

The largest value dividing both, and never a negative one: gcd(-12, 18) is 6.

Euclid’s algorithm over the magnitudes, which is what makes one loop serve every combination of signs: the divisors of a number and of its negation are the same set, so the signs can be dropped at the door rather than reasoned about. gcd(x, 0) is the magnitude of x, everything dividing zero, and gcd(0, 0) is zero.

The binary algorithm – shifts and subtractions, no division – is the faster one on a machine whose divide is slow, and it is a fair amount more code for a constant factor. This is the one that is obviously right.

is_even

is_even(x: BigInt) -> bool

Whether two divides the value. Zero is even, as it is everywhere else.

is_odd

is_odd(x: BigInt) -> bool

Whether two does not divide the value.

isqrt

isqrt(x: BigInt) -> BigInt

The largest value whose square is at most x: isqrt(17) is 4, and isqrt(16) is 4.

Newton’s method on the integers, and it lands exactly – there is no tolerance to choose and no epsilon anywhere in it. Starting from a guess at or above the answer, each step (g + x/g) / 2 is still at or above it, and the sequence decreases until it cannot; the first step that fails to decrease is the one that has arrived, and the value before it is the floor. That is the standard argument and it needs the starting guess to be an over-estimate, which is what the shift gives: two raised to half the bit length, rounded up, is at least the root.

A negative value traps, there being no integer answer and no reason for this to be the one function in the module that returns an Option for a mistake the caller can see coming.

lcm

lcm(a: BigInt, b: BigInt) -> BigInt

The smallest non-negative value both divide: lcm(4, 6) is 12.

Divided before multiplied – a / gcd(a, b) * b rather than a * b / gcd(a, b). The two agree on every input, gcd being a divisor of a so the quotient is exact, and the second builds an intermediate with as many limbs as both operands together. That costs nothing at a machine width and costs a real allocation here.

A zero operand answers zero, which is both the right answer – zero is a multiple of everything – and the case that would otherwise divide by gcd(0, 0).

mod_floor

mod_floor(a: BigInt, b: BigInt) -> BigInt

a modulo b, taking the divisor’s sign: mod_floor(-7, 2) is 1.

mod_pow

mod_pow(base: BigInt, exp: BigInt, m: BigInt) -> BigInt

base raised to exp, modulo m, by square-and-multiply: mod_pow(2, 10, 1000) is 24.

The reduction is what makes it possible at all. pow(base, exp) on its own grows to roughly exp * bit_length(base) bits, so an exponent of a few thousand is a number nobody can hold; reducing after every squaring keeps every intermediate under twice the modulus.

The answer takes the modulus’s sign, which is mod_floor‘s rule and Python’s: it lands in 0..<m for a positive m. A negative exponent traps – base^-1 modulo m is the modular inverse, which exists only where base and m are coprime and is a different function with a different failure. A zero modulus traps, as a division by it does.

This is NOT constant-time and must not be used where the exponent is a secret. The loop multiplies on a set bit and does not on a clear one, so the time it takes reveals how many bits of the exponent are set, and a cache-timing attacker learns more than that. A constant-time modular exponentiation is a Montgomery ladder over a Montgomery-form modulus, and it is a separate function rather than a flag on this one – saying which one a caller got is the point.

mul

mul(a: BigInt, b: BigInt) -> BigInt

a * b, schoolbook. Allocates.

The sign is the sign of the product and the magnitudes multiply, with the one special case every sign-and-magnitude implementation has: a zero result must not be negative, and here it cannot be, because a zero operand returns early.

mul_long

mul_long(a: BigInt, n: long) -> BigInt

a * n, one pass over the limbs where n fits a limb.

The magnitude of the operand is taken as a u64 before the width is tested, which is what lets the most negative long take the general route rather than wrapping on the way to it.

negate

negate(x: BigInt) -> BigInt

The same magnitude with the other sign, and zero unchanged – there being no negative zero here to produce.

not

not(x: BigInt) -> BigInt

~x, which on an infinitely wide two’s-complement number is -x - 1: ~5 is -6 and ~(-1) is 0.

It is written as the subtraction rather than as a complement loop, because that is what it is: inverting every bit of an endless run of them is the identity -x - 1 and nothing else, and a loop over the limbs would have to invent a width to stop at.

one

one() -> BigInt

One.

or

or(a: BigInt, b: BigInt) -> BigInt

a | b, on the two’s-complement reading – so (-6) | 3 is -5. Allocates.

Negative wins, by the same reading and takes from the other side: the sign bit is set where either operand has it, so one negative operand makes the answer negative.

parse

parse(s: string) -> Option[BigInt]

The value s names in base ten, or None.

parse_radix

parse_radix(s: string, base: int) -> Option[BigInt]

The value s names in base, or None where it names none.

An optional + or - and then at least one digit, in either case. Nothing else: no underscores, no 0x prefix, no leading or trailing space. A caller reading a number out of a document trims and strips before it gets here, because what a document allows is the document’s business.

"-0" parses to zero and renders as "0", which is the one input that does not round trip and could not: there is no negative zero in this representation, which is what makes equality and hashing straightforward everywhere else.

pow

pow(a: BigInt, n: usize) -> BigInt

a raised to n, by repeated squaring. Allocates once per squaring and once per set bit.

A negative exponent has no integer answer and gives one, since x^-1 is not an integer for any x but 1 and -1; a caller wanting a rational wants a rational. pow(x, 0) is one, including for x of zero, which is the convention every language and every algebra text takes.

rem

rem(a: BigInt, b: BigInt) -> BigInt

a % b, taking the dividend’s sign.

shl

shl(x: BigInt, n: usize) -> BigInt

x << n, which is x * 2^n and keeps the sign. Allocates.

There is no overflow to think about, which is the whole difference from a machine integer: a shift left grows the number rather than discarding what leaves the top.

shr

shr(x: BigInt, n: usize) -> BigInt

x >> n, arithmetic: the result floors toward negative infinity, so (-1) >> 100 is -1 and (-7) >> 1 is -4. Allocates.

Flooring is what the two’s-complement reading forces, and it is worth seeing why rather than taking it as a convention. Shifting an endless run of bits right discards the low ones and drags the sign down from above, which for a negative number is exactly division rounding away from zero in magnitude. A truncating shift would disagree with >> on every machine integer in the language and with div_floor beside it, and there would then be two different meanings of the same symbol.

So the magnitude is shifted and rounded up where anything was lost, and the sign put back.

sub

sub(a: BigInt, b: BigInt) -> BigInt

a - b, which is a + (-b) and is written as that rather than as a second carry loop.

sub_long

sub_long(a: BigInt, n: long) -> BigInt

a - n.

Written as a subtraction rather than as add_long(a, -n), which would be wrong at the most negative long: that value has no positive counterpart, so negating it in a long wraps back to itself and the answer would come out added instead of taken away.

test_bit

test_bit(x: BigInt, i: usize) -> bool

Whether bit i is set, on the two’s-complement reading – so every bit of -1 is set, and every bit above the magnitude of a negative number is too.

A negative value is read through |x| - 1 rather than complemented whole: -m is ~(m - 1), so a bit of the negative is the complement of the same bit of m - 1, and one subtraction answers any index including the ones past the end. Allocates for a negative x and not for a positive.

to_real

to_real(x: BigInt) -> real

The nearest real to the value, ties to even, and an infinity where the value is past what a real holds.

Every step of the rounding is done on the integer side, where it is exact, and the result is assembled by a multiplication that cannot itself round: a mantissa of at most 53 bits converts exactly, and scaling it by a power of two is exact until it overflows – at which point it becomes an infinity, which is the right answer.

to_string

to_string(x: BigInt) -> string

x in base ten.

to_string_radix

to_string_radix(x: BigInt, base: int) -> string

x written in base, with a leading - where it is negative and no + where it is not.

A base outside 2 to 36 traps, since there is no digit to write past z and a caller asking for base 37 has a mistake rather than an unusual requirement. Zero is "0" in every base.

Allocates the answer, and a working buffer the size of the answer.

to_u64

to_u64(x: BigInt) -> Option[u64]

The value as a u64, or None where it is negative or too large.

A negative value answers None rather than its two’s-complement pattern, which is the one decision this makes: the bit operations in this module read a negative number as an endless run of ones, and no truncation of that to 64 bits is more obviously right than any other. A caller that wants the low word of a two’s-complement reading writes and(x, from_u64(0xffffffffffffffff)) and gets a value that says what it is.

trailing_zeros

trailing_zeros(x: BigInt) -> usize

How many low bits are clear, which is how many times two divides the value.

Zero answers zero, there being no highest power of two dividing it and no honest number to give; a caller that cares tests is_zero first. The sign does not matter: complementing a value leaves its trailing zeros exactly where they were, since -m is ~(m - 1) and the borrow runs out at the lowest set bit.

xor

xor(a: BigInt, b: BigInt) -> BigInt

a ^ b, on the two’s-complement reading – so (-6) ^ 3 is -7. Allocates.

The answer is negative when exactly one operand is, which is the sign bit obeying the same rule every other bit does.

zero

zero() -> BigInt

Zero, which is the empty magnitude.

Types

BigInt

struct BigInt
    private[bigint] minus: bool
    private[bigint] mag: []const u32

An integer of no fixed width.

The magnitude is a view, so a BigInt costs a retain to copy rather than a copy of its limbs. That is what makes passing one around cheap and is the reason the limbs are never written through: every operation builds a new magnitude, so two values may share one and neither can see the other change.

MemberSignatureDescription
is_zerois_zero(self) -> boolWhether this is zero, which is the one value with an empty magnitude.
signsign(self) -> int-1, 0 or 1, which is the comparison against zero written as a number – what a caller switching on the three cases wants, and what cmp answers for a pair.
limbslimbs(self) -> usizeHow many limbs the magnitude takes, which is the closest thing to a size this type has.
to_longto_long(self) -> Option[long]The value as a long, or None where it does not fit.

Implementations

Add for BigInt

impl Add for BigInt

BitAnd for BigInt

impl BitAnd for BigInt

BitOr for BigInt

impl BitOr for BigInt

BitXor for BigInt

impl BitXor for BigInt

Display for BigInt

impl Display for BigInt

Div for BigInt

impl Div for BigInt

Eq for BigInt

impl Eq for BigInt

From for BigInt

impl From[u64] for BigInt

From for BigInt

impl From[long] for BigInt

Hash for BigInt

impl Hash for BigInt

Mul for BigInt

impl Mul for BigInt

Neg for BigInt

impl Neg for BigInt

Not for BigInt

impl Not for BigInt

One for BigInt

impl One for BigInt

Ord for BigInt

impl Ord for BigInt

Written out, and it stays written out now that an array has Ord – a BigInt is not structurally ordered.* Its fields are a sign and a little-endian limb view, so a derived comparison would take the sign first, putting every positive before every negative, and then compare limbs from the least significant end, making 2^32 less than 1. The numeric order needs the magnitudes’ lengths first and their limbs from the top, which is cmp_mag.

It could not be derived in any case: bool has no Ord, so derives Ord on this shape is refused with '<' is not defined for bool.

Rem for BigInt

impl Rem for BigInt

Shl for BigInt

impl Shl[usize] for BigInt

Shr for BigInt

impl Shr[usize] for BigInt

Sub for BigInt

impl Sub for BigInt

Zero for BigInt

impl Zero for BigInt

Search

Esc
↑↓ to navigate ↵ to open Esc to close