BigInt

Integers of any size, and the fixed-width views of them if you need them.

A Gren Int is a double, so it is exact up to 2^53 and wrong above that. That is fine until you are working out what a uint64 did, and then it is the whole problem: 0xFFFFFFFFFFFFFFFF is not representable, and what comes back instead is 18446744073709552000, with nothing to say so.

A BigInt has no width. Instead, it has a way to ask for one:

-- what does this look like as a uint64?
BigInt.maskTo 64 x

-- ...and as an int64?
BigInt.toSigned 64 x

-- would it have overflowed?
BigInt.fitsSigned 64 x

The width is a question you put to a value, not a property the value carries around and can silently violate.

type BigInt

An integer of any size.

The representation is sign and magnitude rather than two's complement: a Bool for the sign, and an array of 24-bit limbs for the magnitude, least significant first, with no leading zero limbs. Zero is the empty array, and is never negative.

Limbs smaller than the range a double holds exactly is the standard way to build this. CPython uses 30-bit digits and bn.js uses 26, and the reason is multiplication: a limb times a limb plus a carry has to stay under 2^53. What is particular here is the 24. It gives up two of bn.js's bits for a different property: 24 is divisible by 1, 2, 3 and 4, so binary, octal and hexadecimal all land on limb boundaries. One limb is exactly six hex digits, eight octal digits, or twenty-four binary digits, so toStringWithBase writes those bases by slicing the limbs apart rather than by dividing the number down (see sliceLimbs). Every other base, base 32 included, has to divide. The README says where all of this comes from; the algorithms are Knuth's.

Two's complement does appear, in and and its neighbors, which read a value as if it were two's complement of unbounded width. That is the only reading under which complement 5 can be -6 when nobody has said how wide the number is.

Making them

zero : BigInt

Zero.

This is the value the whole representation is normalized around: a magnitude that cancels becomes the empty limb array, and an empty limb array is never negative. So there is exactly one zero, and x |> subBy x == BigInt.zero is a reliable test.

It is also the divisor that makes every division in this module return Nothing.

one : BigInt

One.

Mostly the step: x |> add one and x |> subBy one. It is also the empty product, so powBy 0 returns it for any base.

fromInt : Int -> BigInt

An ordinary Int. Above 2^53 an Int is already inexact, so this converts the double you passed, not the number you meant.

toInt : BigInt -> Maybe Int

Back to an Int, when it fits in one exactly, which means no larger than Math.maxSafeInteger. Anything bigger is Nothing, not a number that is almost right.

fromFloat : Float -> Maybe BigInt

Truncate a Float toward zero. NaN and the infinities are Nothing. Everything else has an exact integer part, however large, because past a certain exponent a double is an integer.

Toward zero rather than floored, the same split as quotRemBy against divModBy:

BigInt.fromFloat -3.7 --> Just (BigInt.fromInt -3)

The result is exact, not approximate. Example:

BigInt.fromFloat 1.0e30 |> Maybe.map BigInt.toString
--> Just "1000000000000000019884624838656"

Those trailing digits are not noise. That is the integer the double has held all along, and this is the function that shows you which one you actually have. toFloat is the direction that loses it again.

toFloat : BigInt -> Float

To a Float, rounding once the value no longer fits.

This is the lossy half of the round trip, and above 2^53 it is lossy quietly:

BigInt.toFloat (BigInt.add big BigInt.one) == BigInt.toFloat big
-- True for big = 2^64, and nothing says the one went missing

fromFloat loses nothing going the other way, so a value survives a round trip through a String but not through a Float. If you need to store one, store the string.

Reading and writing

fromString : String -> Maybe BigInt

Read a number written the way it would be in source: an optional sign, then 0x, 0b or 0o and its digits, or plain decimal.

An underscore between two digits is ignored, so 0xdead_beef and 1_000_000 both parse, and so does 1__000. An underscore anywhere else is not a separator, so _1, 1_ and 0x_ff are Nothing.

BigInt.fromString "0b1010" --> Just (BigInt.fromInt 10)

BigInt.fromString "-0xff" --> Just (BigInt.fromInt -255)

BigInt.fromString "12 apples" --> Nothing
toString : BigInt -> String

Decimal, with a leading - when negative.

fromStringWithBase : Int -> String -> Maybe BigInt

Read a number in a base from 2 to 36, with an optional sign and no prefix. Underscores between digits are ignored here too.

toStringWithBase : Int -> BigInt -> String

In a base from 2 to 36, with digits 0-9 then a-z, and a leading - when negative. No prefix: toStringWithBase 16 gives "ff", not "0xff".

A negative number is written with a sign rather than as two's complement, so toStringWithBase 16 (fromInt -255) is "-ff". If you want the bit pattern a machine would show for a 64-bit value, apply maskTo first; that is what it is for.

Anything outside 2 to 36 gives "".

Reading text you did not write

fromStringWithin : Int -> String -> Result Error BigInt

fromString with a ceiling on how many digits the number may have, for text that comes from somewhere you do not control: fromStringWithin limit text.

The plain fromString does what it is told. A string of a million digits is a million-digit number, and parsing it costs time that grows with the square of its length. When the text is untrusted, say how big a number you are prepared to accept, and anything longer is refused before any arithmetic starts:

BigInt.fromStringWithin 20 "18446744073709551615"
    |> Result.map BigInt.toString
--> Ok "18446744073709551615"

BigInt.fromStringWithin 20 "123456789012345678901"
--> Err (BigInt.TooLong { digits = 21, limit = 20 })

BigInt.fromStringWithin 20 "twelve"
--> Err BigInt.NotANumber

The digits are counted in the base the text is written in, after the sign, the prefix, any separators and any leading zeroes, so 0x0000ff is two digits. Zero is one digit. A limit below one refuses everything. Malformed text is NotANumber whatever its length.

fromStringWithBaseWithin : Int -> Int -> String -> Result Error BigInt

fromStringWithBase with the same ceiling as fromStringWithin: fromStringWithBaseWithin base limit text. The digits are counted in base, so the same limit admits a longer number in hexadecimal than in binary.

BigInt.fromStringWithBaseWithin 16 4 "dead"
    |> Result.map BigInt.toString
--> Ok "57005"

BigInt.fromStringWithBaseWithin 2 4 "11111"
--> Err (BigInt.TooLong { digits = 5, limit = 4 })

A base outside 2 to 36 is NotANumber, as it is Nothing for the plain function.

type Error
= NotANumber
| TooLong ({ digits : Int, limit : Int })

Why fromStringWithin refused.

NotANumber is everything the plain fromString returns Nothing for. TooLong is a well-formed number that needs more digits than the limit allows, and it says how many.

Arithmetic

add : BigInt -> BigInt -> BigInt

Add. There is no overflow to think about.

No By suffix, unlike subBy, because addition is commutative, so there is no subject for a pipeline to put last.

Sign and magnitude means this is only sometimes an addition: when the signs differ it is the smaller magnitude subtracted from the larger, and the answer takes the sign of whichever was larger.

subBy : BigInt -> BigInt -> BigInt

Subtract the first from the second: subBy b a is a - b.

a |> BigInt.subBy one
mul : BigInt -> BigInt -> BigInt

Multiply, exactly, however big the answer gets. Commutative, so no By.

The sign is whether the two signs differed. The magnitudes go through long multiplication, a row at a time, so the cost is the product of the two lengths.

negate : BigInt -> BigInt

Flip the sign. The limbs are handed through untouched: no arithmetic happens here at any size, only the sign changes.

negate zero is zero: it goes through make, which never produces a negative zero, so there is only ever one.

abs : BigInt -> BigInt

Drop the sign. Again the limbs pass through untouched.

This one cannot fail. abs of the most negative value is the classic fixed-width bug: there is no positive Int.minValue to return, so it comes back negative. A BigInt has no most negative value, so the bug has nowhere to happen.

powBy : Int -> BigInt -> BigInt

Raise to a power: powBy exponent base.

A negative exponent is not an error. It gives the truncated integer answer, the same as every other division in this module: two to the minus three is an eighth, truncated to zero; one to any negative power is one; and zero to a negative power, the only genuinely undefined case, is zero.

BigInt.fromInt 2 |> BigInt.powBy 10 --> BigInt.fromInt 1024

Dividing

There are two, because C and Python define division differently and you are usually checking one of them. Each comes in three forms: the pair, and either half on its own.

quotRemBy :
BigInt
-> BigInt
-> Maybe { quotient : BigInt, remainder : BigInt
}

Divide, truncating toward zero, so the remainder takes the sign of the dividend: quotRemBy divisor dividend. This is C's / and %, and Gren's own // and Math.remainderBy.

-7 / 2 is -3 remainder -1 here.

Nothing when the divisor is zero, which is the only way it can fail.

quotBy : BigInt -> BigInt -> Maybe BigInt

The quotient alone, truncating toward zero: quotBy divisor dividend. This is Gren's //, except that it is still correct above 2^31.

BigInt.fromInt -7 |> BigInt.quotBy (BigInt.fromInt 2)
--> Just (BigInt.fromInt -3)

Nothing when the divisor is zero.

remainderBy : BigInt -> BigInt -> Maybe BigInt

The remainder alone, taking the sign of the dividend: remainderBy divisor dividend. This is Math.remainderBy.

BigInt.fromInt -7 |> BigInt.remainderBy (BigInt.fromInt 2)
--> Just (BigInt.fromInt -1)

Nothing when the divisor is zero.

divModBy :
BigInt
-> BigInt
-> Maybe { quotient : BigInt, modulus : BigInt
}

Divide, flooring, so the modulus takes the sign of the divisor: divModBy divisor dividend. This is Python's // and %, and Gren's own Math.modBy.

-7 / 2 is -4 modulus 1 here.

The two differ only when exactly one operand is negative. Which one you want depends on whether you are checking C or Python, so both are here.

divBy : BigInt -> BigInt -> Maybe BigInt

The floored quotient alone: divBy divisor dividend. Python's //.

BigInt.fromInt -7 |> BigInt.divBy (BigInt.fromInt 2)
--> Just (BigInt.fromInt -4)

Nothing when the divisor is zero.

modBy : BigInt -> BigInt -> Maybe BigInt

The modulus alone, taking the sign of the divisor: modBy divisor dividend. This is Math.modBy, and it is the one to reach for when the question is divisibility, because its answer is zero or has the divisor's sign and never the dividend's.

BigInt.fromInt -7 |> BigInt.modBy (BigInt.fromInt 2)
--> Just (BigInt.fromInt 1)

Nothing when the divisor is zero.

Comparing

compare : BigInt -> BigInt -> Order

How the first compares to the second, the way Basics.compare does.

isZero : BigInt -> Bool

Whether this is zero, which is Array.isEmpty on the limbs and so costs nothing at any size.

x == BigInt.zero gives the same answer, because normalization guarantees a single representation. This one just reads better.

isNegative : BigInt -> Bool

Zero is not negative.

isEven : BigInt -> Bool

Whether this is a multiple of two. Zero is.

This reads only the bottom bit of the bottom limb, since the limbs are little-endian; nothing else in the number is touched. Asking through a division instead (modBy (fromInt 2)) walks every limb and builds a quotient nobody wanted.

The sign does not come into it, because the representation is sign and magnitude and -7 is odd for the same reason 7 is.

isOdd : BigInt -> Bool

Whether this is not a multiple of two.

BigInt.isOdd (BigInt.fromInt -7) --> True
max : BigInt -> BigInt -> BigInt

The larger of the two, standing in for Basics.max.

min : BigInt -> BigInt -> BigInt

The smaller of the two, standing in for Basics.min.

Bits

and : BigInt -> BigInt -> BigInt

Bitwise AND.

or : BigInt -> BigInt -> BigInt

Bitwise OR.

xor : BigInt -> BigInt -> BigInt

Bitwise XOR.

complement : BigInt -> BigInt

Flip every bit, which for a number with no width is exactly -x - 1.

BigInt.complement (BigInt.fromInt 5) --> BigInt.fromInt -6
shiftLeftBy : Int -> BigInt -> BigInt

Shift left, which is multiplication by a power of two and never loses anything. A negative count shifts the other way.

shiftRightBy : Int -> BigInt -> BigInt

Shift right arithmetically: the sign is kept and the result is floored, so -1 shifted right stays -1 rather than becoming 0. That is what a machine's arithmetic shift does and what two's complement means. The truncating version is quotRemBy with a power of two.

bitLength : BigInt -> Int

How many bits the magnitude needs; zero for zero. The sign is not counted, because a BigInt has no width for a sign bit to live in.

BigInt.fromInt 255 |> BigInt.bitLength --> 8
popCount : BigInt -> Int

How many bits of the magnitude are set. Counting a negative number's bits would mean counting an infinite run of ones, so this counts the magnitude's and says so; maskTo first if that is not what you meant.

Widths

maskTo : Int -> BigInt -> BigInt

The low width bits, as a number that is never negative, which is what reading a value as an unsigned integer of that width means.

BigInt.fromInt -1 |> BigInt.maskTo 64 |> BigInt.toString
    --> "18446744073709551615"
toSigned : Int -> BigInt -> BigInt

The low width bits read as two's complement, which is what reading a value as a signed integer of that width means.

BigInt.fromString "0xFFFFFFFFFFFFFFFF"
    |> Maybe.map (BigInt.toSigned 64)
    --> Just (BigInt.fromInt -1)
fitsSigned : Int -> BigInt -> Bool

Would this survive being stored in a signed integer of this width? That is, is it within -2^(width-1) to 2^(width-1) - 1?

fitsUnsigned : Int -> BigInt -> Bool

Would this survive being stored in an unsigned integer of this width?

A note on argument order

Wherever the order of the operands matters, the subject comes last. This matches Math.modBy, Math.remainderBy and Bitwise.shiftLeftBy in core:

a |> BigInt.subBy one -- a - 1

a |> BigInt.quotRemBy (BigInt.fromInt 2) -- a / 2

a |> BigInt.shiftLeftBy 8 -- a << 8

compare is the exception. It stands in for Basics.compare, so it takes its arguments in the same order that one does.