Skip to content

Decimo — User Manual ​

Decimo v0.16.0

This page is the manual in docs/user_manual.md of the Decimo repository, at v0.16.0.

A guide to the Decimo arbitrary-precision arithmetic library for Mojo.

All code examples below assume that you have imported the prelude at the top of your Mojo file:

mojo
from decimo.prelude import *

Installation ​

Decimo is available in the modular-community package repository. Add it to your channels list in pixi.toml:

toml
channels = ["https://conda.modular.com/max", "https://repo.prefix.dev/modular-community", "conda-forge"]

Then install:

bash
pixi add decimo

Or add it manually to pixi.toml:

toml
decimo = ">=0.16.0, <0.17.0"

Then run pixi install.

Quick Start ​

mojo
from decimo.prelude import *


def main() raises:
    # Arbitrary-precision integer
    var a = BInt("12345678901234567890")
    var b = BInt(42)
    print(a * b)          # 518518513851851851380
    print(BInt(2) ** 256)  # 2^256, all 78 digits

    # Arbitrary-precision decimal
    var x = Decimal("123456789.123456789")
    var y = Decimal("1234.56789")
    print(x + y)                          # 123458023.691346789 (exact)
    print(x.true_divide(y, precision=50)) # 50 significant digits
    print(x.sqrt(precision=100))          # 100 significant digits
    print(Decimal.pi(precision=1000))     # 1000 digits of π

Part I — BigInt (BInt) ​

Overview ​

BigInt (alias BInt) is an arbitrary-precision signed integer type — the Mojo-native equivalent of Python's int. It supports unlimited-precision integer arithmetic, bitwise operations, and number-theoretic functions.

PropertyValue
Full nameBigInt
AliasBInt
Internal base2^64 (binary representation)
Word typeUInt64 (little-endian)
Python equivalentint

Construction ​

From zero ​

mojo
var x = BInt()          # 0

From Int ​

mojo
var x = BInt(42)
var y = BInt(-100)
var z: BInt = 42        # Implicit conversion from Int

The constructor is marked @implicit, so Mojo can automatically convert Int to BInt where expected.

From String ​

mojo
var a = BInt("12345678901234567890")  # Basic decimal string
var b = BInt("-98765")                 # Negative number
var c = BInt("1_000_000")             # Underscores as separators
var d = BInt("1,234,567")             # Commas as separators
var e = BInt("1.23e5")                # Scientific notation (= 123000)
var f = BInt("1991_10,18")            # Mixed separators (= 19911018)

Note: The string must represent an integer. BInt("1.5") raises an error. Scientific notation like "1.23e5" is accepted only if the result is an integer.

From Scalar (any integral SIMD type) ​

mojo
var x = BInt(UInt32(42))
var y = BInt(Int64(-5))
var z: BInt = UInt32(99)     # Implicit conversion

Accepts any integral scalar type (Int8 through Int256, UInt8 through UInt256, etc.).

Summary of constructors ​

ConstructorDescription
BInt()Zero
BInt(value: String)From decimal string (raises)
BInt(value: Scalar)From any integral scalar (implicit)
BInt.from_integral_scalar(value)From any integral scalar type
BInt.from_string(value)Explicit factory from string (raises)
BInt.from_biguint(value, sign)From a base-10^18 magnitude and a sign

Unsafe constructors ​

These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid.

ConstructorDescription
BInt(uninitialized_capacity=n)Empty words list with reserved capacity
BInt(raw_words=Magnitude, sign=Bool)From raw words, no leading-zero stripping

Magnitude is BInt's word storage, exported from decimo. Its words are UInt64 and its base is 2^64; before v0.14.0 this was a List[UInt32] in base 2^32, so a literal is now [UInt64(1)], and an existing List goes in as BInt(raw_words=Magnitude(words^), sign=False).

Arithmetic Operations ​

Binary operators ​

ExpressionDescriptionRaises?
a + bAdditionNo
a - bSubtractionNo
a * bMultiplicationNo
a / bTruncating division (toward zero)Yes (zero div)
a // bFloor division (rounds toward −∞)Yes (zero div)
a % bFloor modulo (Python semantics)Yes (zero div)
divmod(a,b)Floor quotient and remainderYes (zero div)
a ** bExponentiationYes (negative exp)

Unary operators ​

ExpressionDescription
-aNegation
+aUnary plus (returns copy)
abs(a)Absolute value
if a:Nonzero test (Bool(a) is not supported)
~aBitwise NOT (two's complement)

In-place operators ​

+=, -=, *=, //=, %=, <<=, >>= are all supported and perform true in-place mutation to reduce memory allocation.

mojo
var a = BInt("12345678901234567890")
var b = BInt(12345)
print(a + b)   # 12345678901234580235
print(a - b)   # 12345678901234555545
print(a * b)   # 152407406035740740602050
print(a // b)  # 1000054994024671
print(a % b)   # 4395
print(BInt(2) ** 10)  # 1024

Division Semantics ​

BigInt supports two division conventions:

NameOperator / MethodQuotientPython equivalent
Floor division//, %, divmod()Toward −∞//, %
Truncate division/, .truncate_divide(), .truncate_modulo()Toward zeroC/Java /, %

/ on a BInt is integer division, not a promotion to a decimal type. It follows Mojo's own Int, where Int(7) / Int(-2) is -3: the quotient stays in the type and rounds toward zero. It is therefore a different operator from //, not a synonym, and the two disagree exactly when the operands have opposite signs.

The difference matters for negative operands:

mojo
var a = BInt(7)
var b = BInt(-2)

# Floor division (Python semantics)
print(a // b)                    # -4
print(a % b)                     # -1

# Truncate division (C/Java semantics)
print(a / b)                     # -3
print(a.truncate_divide(b))      # -3
print(a.truncate_modulo(b))      #  1

Comparison ​

All six comparison operators (==, !=, >, >=, <, <=) are supported. Each accepts both BInt and Int as the right operand.

mojo
var a = BInt("12345678901234567890")
print(a > 1000)        # True
print(a == BInt("12345678901234567890"))  # True
print(a != 0)          # True

Additional methods:

mojo
a.compare(b)              # Returns Int8: 1, 0, or -1
a.compare_magnitudes(b)   # Compares |a| vs |b|

Bitwise Operations ​

All bitwise operations follow Python's two's complement semantics for negative numbers.

OperatorDescription
a & bBitwise AND
a | bBitwise OR
a ^ bBitwise XOR
~aBitwise NOT:  x=−(x+1)

Each accepts both BInt and Int as the right operand. In-place variants (&=, |=, ^=) are also available.

mojo
var a = BInt(0b1100)
var b = BInt(0b1010)
print(a & b)   # 8   (0b1000)
print(a | b)   # 14  (0b1110)
print(a ^ b)   # 6   (0b0110)
print(~a)      # -13

# Negative numbers use two's complement:
print(BInt(-1) & BInt(255))  # 255

Shift Operations ​

OperatorDescription
a << nLeft shift (multiply by 2n)
a >> nRight shift (floor divide by 2n)
mojo
var x = BInt(1)
print(x << 100)         # 1267650600228229401496703205376 (= 2^100)
print(BInt(1024) >> 5)  # 32

Mathematical Functions ​

Exponentiation ​

mojo
print(BInt(2).power(100))    # 2^100
print(BInt(2) ** 100)         # Same via ** operator

Both power(exponent: Int) and power(exponent: BigInt) are supported. The exponent must be non-negative.

Integer square root ​

mojo
var x = BInt("100000000000000000000")
print(x.sqrt())    # 10000000000 (largest y such that y² ≤ x)
print(x.isqrt())   # Same as sqrt()

Raises if the value is negative.

Number Theory ​

All number-theory operations are available as both instance methods and free functions:

mojo
from decimo import BInt, gcd, lcm, extended_gcd, mod_pow, mod_inverse

GCD — Greatest Common Divisor ​

mojo
var a = BInt(48)
var b = BInt(18)
print(a.gcd(b))      # 6
print(gcd(a, b))      # 6 (free function)

LCM — Least Common Multiple ​

mojo
print(BInt(12).lcm(BInt(18)))    # 36
print(lcm(BInt(12), BInt(18)))   # 36

Extended GCD ​

Returns (g, x, y) such that a*x + b*y = g:

mojo
var result = BInt(35).extended_gcd(BInt(15))
# result = (5, 1, -2)   — since 35×1 + 15×(−2) = 5

Modular Exponentiation ​

Computes (baseexp)modm efficiently without computing the full power:

mojo
print(BInt(2).mod_pow(BInt(100), BInt(1000000007)))
print(mod_pow(BInt(2), BInt(100), BInt(1000000007)))  # free function

Modular Inverse ​

Finds x such that (a⋅x)≡1(modm):

mojo
print(BInt(3).mod_inverse(BInt(7)))      # 5 (since 3×5 = 15 ≡ 1 mod 7)
print(mod_inverse(BInt(3), BInt(7)))      # 5

Conversion and Output ​

String conversions ​

MethodExample output
String(x)"12345"
repr(x)'BigInt("12345")'
x.to_string_with_separators("_")"1_234_567"
x.to_string_with_separators(",")"1,234,567"
x.to_hex_string()"0x1A2B3C"
x.to_binary_string()"0b110101"
x.to_string(line_width=20)Multi-line output
x.to_chinese()"十五"

Numeric conversions ​

MethodDescription
Int(x)Convert to Int (raises if exceeds 64-bit range)
Float64(x)Convert to Float64 (may lose precision)
x.to_biguint()Convert the magnitude to BigUInt (base-10^18)
mojo
var x = BInt("123456789012345678901234567890")
print(x.to_string_with_separators())  # 123_456_789_012_345_678_901_234_567_890
print(x.to_hex_string())              # 0x...

to_chinese() writes the number in Chinese numerals; see Chinese Numerals for the reading rules and the available styles.

mojo
print(BInt(15).to_chinese())          # 十五
print(BInt("123456789").to_chinese()) # 一亿二千三百四十五万六千七百八十九

Query Methods ​

MethodReturnDescription
x.is_zero()BoolTrue if value is 0
x.is_negative()BoolTrue if value < 0
x.is_positive()BoolTrue if value > 0
x.is_one()BoolTrue if value is 1
x.bit_length()IntNumber of bits in the magnitude
x.bit_count()IntPopulation count (number of set bits)
x.number_of_words()IntNumber of UInt64 words
x.number_of_digits()IntNumber of decimal digits
mojo
var x = BInt(13)
print(x.bit_length())       # 4  (13 = 0b1101)
print(x.bit_count())        # 3  (three 1-bits)
print(x.number_of_digits()) # 2
print(x.is_positive())      # True

Constants and Factory Methods ​

Method / ConstantValue
BInt.zero()0
BInt.one()1
BInt.negative_one()−1
BigInt.ZERO0 (comptime; use materialize[BigInt.ZERO]() at runtime)
BigInt.ONE1 (comptime; use materialize[BigInt.ONE]() at runtime)
BigInt.BITS_PER_WORD64

Part II — Decimal ​

Overview — Decimal ​

Decimal is an arbitrary-precision decimal type — the Mojo-native equivalent of Python's decimal.Decimal. It can represent numbers with unlimited digits and decimal places, making it suitable for financial modeling, scientific computing, and applications where floating-point errors are unacceptable.

PropertyValue
NameDecimal
AliasesBigDecimal, BDec
Internal baseBase-10^18 (each word stores ≤ 18 digits)
Default precision28 significant digits
Python equivalentdecimal.Decimal

Decimal, BigDecimal, and BDec are all the same type. We recommend Decimal for consistency with Python's decimal.Decimal.

How Precision Works ​

  • The default precision is 28 significant digits, matching Python's decimal module.
  • Operators + - * += -= *= (and reflected __radd__ / __rsub__ / __rmul__) round their result HALF_EVEN to the default precision (28 significant digits), matching Python decimal.Decimal default-context arithmetic.
  • Methods .add(other) / .subtract(other) / .multiply(other) (and the in-place variants .add_inplace(other) / .subtract_inplace(other) / .multiply_inplace(other)) take an optional precision: Int = 0 argument. The default precision=0 returns the exact, unrounded result; passing precision > 0 rounds HALF_EVEN to that many significant digits.
  • Division and mathematical functions (sqrt, ln, exp, etc.) accept an optional precision parameter specifying the number of significant digits in the result.
mojo
var a = Decimal("999999999999999999999999999999")  # 30 nines
var b = Decimal("1")

# Operators round to 28 digits (matches Python decimal default context)
print(a + b)             # 1.000000000000000000000000000E+30

# Methods at precision=0 (default) are exact
print(a.add(b))          # 1000000000000000000000000000000

# Methods accept an explicit precision
print(a.add(b, 50))      # 1000000000000000000000000000000 (exact, fits)
print(a.multiply(a, 40)) # 40-significant-digit rounded product
mojo
var x = Decimal("2")
print(x.sqrt())                # 28 significant digits (default)
print(x.sqrt(precision=100))   # 100 significant digits
print(x.sqrt(precision=1000))  # 1000 significant digits

Note: The default precision of 28 will be configurable globally in a future version when Mojo supports global variables.

Construction — Decimal ​

Decimal can be constructed from various types of input using the Decimal() constructor or factory methods. Among these, the most common way is from a string representation of the decimal number, which is the most accurate way to create a Decimal without any precision loss.

From String (Decimal) ​

It is highly recommended to construct Decimal from a string. Please consider using this method whenever possible.

mojo
var a = Decimal("123456789.123456789")  # Basic decimal string
var b = Decimal("1.23E+10")             # Scientific notation
var c = Decimal("-0.000001")            # Negative
var d = Decimal("1_000_000.50")         # Separator support

From zero (Decimal) ​

mojo
var x = Decimal("0")  # Explicitly from string
var y = Decimal()    # Default constructor creates zero, same as Decimal("0")

From Int (Decimal) ​

Although you can construct a Decimal from an Int directly, it is still risky if the Int is so large that it exceeds the maximum value of Int (which is 2^63-1).

mojo
# These work
var x = Decimal(42)     # From Int
var y: Decimal = 100    # Implicit conversion (IntLiteral -> Int -> Decimal)

# This is dangerous!
var z: Decimal = 9223372036854775808  # 2^63, exceeds Int range!
print(z)  # Prints -9223372036854775808, overflowed!

From integral scalars ​

mojo
var x = Decimal(Int64(123456789))
var y = Decimal(UInt128(99999999999999))

Works with all integral SIMD types. Floating-point scalars are rejected at compile time — use from_float_scalar() instead.

From floating-point — from_float_scalar() ​

from_float_scalar() reads the bits of the float and returns the value the float actually holds, not the literal that was written to produce it: from_float_scalar(0.1) is 0.1000000000000000055511151231257827021181583404541015625, which is what decimal.Decimal(0.1) gives as well.

Note that not all decimal numbers can be represented exactly as binary floating-point. You may lose precision without awareness.

To make the conversion from float to Decimal more explicit so that you are aware of the potential precision issues, the Decimal() constructor does not accept floating-point numbers directly. Instead, to create a Decimal from a float, you must use the from_float_scalar() factory method. It accepts any floating-point width — Float16, BFloat16, Float32, Float64 and the 8-bit formats — since every one of them widens to Float64 exactly.

Consider never using Decimal.from_float_scalar() in performance-sensitive code, but use string construction instead.

mojo
var x = Decimal.from_float_scalar(3.14159)
var y = Decimal.from_float_scalar(Float32(2.71828))

from_float() is the former name and still works, forwarding unchanged. It is deprecated in favour of from_float_scalar(), which lines up with from_integral_scalar().

From Python — from_python_decimal() ​

You can always safely construct a Decimal from a Python decimal.Decimal using the from_python_decimal() method without worrying about precision loss.

mojo
from std.python import Python

var decimal = Python.import_module("decimal")
var py_dec = decimal.Decimal("123.456")

var a = Decimal.from_python_decimal(py_dec)
var b = Decimal(py=py_dec)  # Alternative keyword-only syntax

Summary of Decimal constructors ​

ConstructorDescription
Decimal()Zero
Decimal(value: String)From string (raises)
Decimal(value: Scalar)From any integral scalar (impl)
Decimal(py=py_obj)From Python Decimal (raises)
Decimal.from_integral_scalar(value)From any integral scalar type
Decimal.from_float_scalar(value)From any float scalar (raises)
Decimal.from_float(value)Deprecated alias of the above
Decimal.from_string(value)Explicit factory from string
Decimal.from_python_decimal(py_obj)From Python Decimal (raises)

Unsafe Decimal constructors ​

These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid.

ConstructorDescription
Decimal(coefficient: BigUInt, scale: Int, sign: Bool)From raw components
Decimal.from_raw_components(words, scale=0, sign=False)From raw List[BigUInt.Word] words (unsafe)
Decimal.from_raw_components(word, scale=0, sign=False)From a single BigUInt.Word (unsafe)

Decimal Arithmetic ​

The operators +, - and * round their result HALF_EVEN to 28 significant digits, as Python's decimal default context does. Use .add(), .subtract() and .multiply() for the exact, unrounded result; see How Precision Works.

ExpressionDescriptionResult
a + bAdditionRounded to 28 digits
a - bSubtractionRounded to 28 digits
a * bMultiplicationRounded to 28 digits
a / bTrue division (default precision=28)Rounded to precision
a // bTruncated division (toward zero)Integer part
a % bTruncated modulo—
a ** bExponentiation (default precision=28)Rounded to precision
divmod(a,b)Returns (a // b, a % b)—

Built-in integral types are implicitly converted when used in arithmetic:

mojo
var c = Decimal("3.14") + 1        # Int → Decimal
var d = Decimal("100") * UInt(8)   # UInt → Decimal

In-place operators (+=, -=, *=) perform true in-place mutation for reduced allocation.

mojo
var a = Decimal("123456789.123456789")
var b = Decimal("1234.56789")
print(a + b)   # 123458023.691346789
print(a - b)   # 123455554.555566789
print(a * b)   # 152415787654.32099750190521

Division Methods ​

Division is the primary operation where precision matters. Decimo provides several variants:

mojo
var a = Decimal("1")
var b = Decimal("3")
print(a.true_divide(b))                # 0.3333333333333333333333333333 (28 digits)
print(a.true_divide(b, precision=50))  # 50 significant digits
print(a.true_divide(b, precision=200)) # 200 significant digits

Operator / — true division with default precision ​

mojo
var result = a / b  # Same as a.true_divide(b, precision=28)

Operator // — truncated (integer) division ​

mojo
print(Decimal("7") // Decimal("4"))    # 1
print(Decimal("-7") // Decimal("4"))   # -1  (toward zero)

Decimal Comparison ​

All six comparison operators are supported:

mojo
var a = Decimal("123.456")
var b = Decimal("123.4560")  # Same value, different scale
print(a == b)  # True (comparison by value)
print(a > 100) # True (Int implicitly converted)

Additional methods:

mojo
a.compare(b)           # Returns Int8: 1, 0, or -1
a.compare_absolute(b)  # Compares |a| vs |b|
a.max(b)               # Returns the larger value
a.min(b)               # Returns the smaller value

Rounding and Formatting ​

round() — round to decimal places ​

mojo
var x = Decimal("123.456")
print(x.round(2))                       # 123.46 (ROUND_HALF_EVEN)
print(x.round(1))                       # 123.5
print(x.round(0))                       # 123
print(x.round(-1))                      # 1.2E+2
print(x.round(2, ROUND_DOWN))           # 123.45
print(x.round(2, ROUND_UP))            # 123.46

Also works with round() builtin:

mojo
print(round(Decimal("123.456"), 2))  # 123.46

quantize() — match scale of another decimal ​

Adjusts the scale (number of decimal places) to match the scale of exp. The actual value of exp is ignored — only its scale matters.

mojo
var x = Decimal("1.2345")
print(x.quantize(Decimal("0.01")))   # 1.23 (2 decimal places)
print(x.quantize(Decimal("0.1")))    # 1.2  (1 decimal place)
print(x.quantize(Decimal("1")))      # 1    (0 decimal places)

# Currency formatting:
var price = Decimal("19.999")
print(price.quantize(Decimal("0.01")))  # 20.00

normalize() — remove trailing zeros ​

mojo
print(Decimal("1.2345000").normalize())  # 1.2345

__ceil__, __floor__, __trunc__ {#ceil-floor-trunc} ​

mojo
print(Decimal("1.1").__ceil__())    # 2
print(Decimal("1.9").__floor__())   # 1
print(Decimal("-1.9").__trunc__())  # -1

RoundingMode ​

Seven rounding modes are available:

ConstantDescription
ROUND_DOWNTruncate toward zero
ROUND_UPRound away from zero
ROUND_HALF_UPRound half away from zero (traditional)
ROUND_HALF_DOWNRound half toward zero
ROUND_HALF_EVENBanker's rounding (default)
ROUND_CEILINGRound toward +∞
ROUND_FLOORRound toward −∞
mojo
var x = Decimal("2.5")
print(x.round(0, ROUND_HALF_UP))    # 3
print(x.round(0, ROUND_HALF_EVEN))  # 2  (banker's rounding)
print(x.round(0, ROUND_DOWN))       # 2
print(x.round(0, ROUND_UP))         # 3
print(x.round(0, ROUND_CEILING))    # 3
print(x.round(0, ROUND_FLOOR))      # 2

Mathematical Functions — Roots and Powers ​

All mathematical functions accept an optional precision parameter (default=28).

Square root ​

mojo
print(Decimal("2").sqrt())               # 1.414213562373095048801688724
print(Decimal("2").sqrt(precision=100))  # 100 significant digits

Cube root ​

mojo
print(Decimal("27").cbrt())  # 3.000000000000000000000000000
print(Decimal("2").cbrt(precision=50))

Nth root ​

mojo
print(Decimal("256").root(Decimal("8")))    # 2
print(Decimal("100").root(Decimal("3")))    # 4.641588833612778892...

Power / exponentiation ​

mojo
print(Decimal("2").power(Decimal("10")))                 # 1024
print(Decimal("2").power(Decimal("0.5"), precision=50))  # sqrt(2) to 50 digits
print(Decimal("2") ** 10)                                # 1024

Mathematical Functions — Exponential and Logarithmic ​

Exponential (e^x) ​

mojo
print(Decimal("1").exp())                # e ≈ 2.718281828459045235360287471
print(Decimal("10").exp(precision=50))   # e^10 to 50 digits

Natural logarithm ​

mojo
print(Decimal("10").ln(precision=50))    # ln(10) to 50 digits

For repeated calls, a MathCache can be used to avoid recomputing cached constants:

mojo
from decimo.bigdecimal.exponential import MathCache

var cache = MathCache()
var r1 = x1.ln(100, cache)
var r2 = x2.ln(100, cache)  # Reuses cached ln(2) and ln(1.25)

Logarithm with arbitrary base ​

mojo
print(Decimal("100").log(Decimal("10")))  # 2
print(Decimal("8").log(Decimal("2")))     # 3

Base-10 logarithm ​

mojo
print(Decimal("1000").log10())  # 3 (exact for powers of 10)
print(Decimal("2").log10(precision=50))

Mathematical Functions — Trigonometric ​

All trigonometric functions take inputs in radians and accept an optional precision parameter.

Basic functions ​

mojo
print(Decimal("0.5").sin(precision=50))
print(Decimal("0.5").cos(precision=50))
print(Decimal("0.5").tan(precision=50))

Reciprocal functions ​

mojo
print(Decimal("1").cot(precision=50))   # cos/sin
print(Decimal("1").csc(precision=50))   # 1/sin
print(Decimal("1").sec(precision=50))   # 1/cos

Inverse functions ​

mojo
print(Decimal("1").arctan(precision=50))  # π/4 to 50 digits

A Small Argument and a Directed Mode ​

Each of these functions comes to a simple value as its argument goes to nought: the sine to the argument itself, the cosine to one, the cosecant to one over the argument. Every one of those is a value the destination holds exactly, and that is awkward for a function that decides its rounding rather than assuming it. Deciding means evaluating wider than asked until every value the error allows rounds the same way, and an answer that lands on a value the destination holds has values on both sides of it that round differently, however wide the evaluation goes.

The three nearest modes do not notice, since the answer is that value either way. The four directed modes do, because they have to say which side of it the truth lies on.

mojo
from decimo import Decimal, RoundingMode

var tiny = Decimal("1E-500")
print(tiny.sin(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 9.9999999999999999999E-501 -- the sine is below its argument
print(tiny.tan(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 1.0000000000000000000E-500 -- the tangent is above it
print(tiny.cos(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 0.99999999999999999999 -- the cosine is below one

So such an argument is answered from the sign of the function's own first correction term, which is all the rounding needs: sin(x) = x - x^3/6 is below x and tan(x) = x + x^3/3 is above it, and neither correction can be computed at 1E-500 by any width a loop would reach.

A small argument is not the only place this happens. Wherever a function's value is a whole number, an argument a hair away from that point has an answer a hair away from a value the destination holds, and the same thing goes wrong. log10(1000) is three exactly; log10 of a thousand plus 1E-500 is three plus a correction five hundred digits down.

mojo
from decimo import Decimal, RoundingMode

var beside = Decimal("1000").add(Decimal("1E-500"))
print(beside.log10(precision=20, rounding_mode=RoundingMode.ROUND_UP))
# 3.0000000000000000001 -- above three, because the argument is above a thousand
print(beside.log10(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 3.0000000000000000000 -- and the same value truncated is three

BigFloat has the same answers for ln beside one, log2 and log10 beside a power of their base, and exp2 and exp10 beside a whole argument.

Mathematical Constants ​

π (pi) ​

Computed using the Chudnovsky algorithm with binary splitting:

mojo
print(Decimal.pi(precision=100))    # 100 digits of π
print(Decimal.pi(precision=1000))   # 1000 digits of π

e (Euler's number) ​

Computed as exp(1):

mojo
print(Decimal.e(precision=100))     # 100 digits of e
print(Decimal.e(precision=1000))    # 1000 digits of e

Decimal Conversion and Output ​

String output ​

The to_string() method provides flexible formatting:

mojo
var x = Decimal("123456789.123456789")
print(x)                                          # 123456789.123456789
print(x.to_string(scientific=True))               # 1.23456789123456789E+8
print(x.to_string(engineering=True, force_exponent=True))  # 123.456789123456789E+6
print(x.to_string(delimiter="_"))                 # 123_456_789.123_456_789
print(x.to_string(line_width=20))                 # Multi-line output
print(x.to_string(force_plain=True))              # Suppress auto-scientific notation

Default output follows CPython's Decimal.__str__() rules: plain notation when feasible, scientific notation when there would be more than 6 leading zeros or the exponent is positive.

Dec128 does not switch: it is bounded to 29 digits and a scale of 0 to 28, so plain notation is never longer than 30 characters, and it always prints plain (the .NET System.Decimal rule). The two types therefore print the same value differently below 1E-6 and at a positive exponent:

mojo
print(Decimal("1E-7"))                  # 1E-7
print(Dec128("1E-7"))                   # 0.0000001
print(Decimal("1E+28"))                 # 1E+28
print(Dec128("1E+28"))                  # 10000000000000000000000000000
print(Decimal("1E-6"), Dec128("1E-6"))  # 0.000001 0.000001

To get the other form: Decimal.to_string(force_plain=True) and Dec128.to_string(scientific=True).

Convenience aliases:

mojo
x.to_scientific_string()               # to_string(scientific=True)
x.to_eng_string()                      # to_string(engineering=True)
x.to_string_with_separators("_")       # to_string(delimiter="_")

to_chinese() writes the number in Chinese numerals; see Chinese Numerals for the reading rules and the available styles.

mojo
print(Decimal("1050.07").to_chinese())  # 一千零五十点零七

repr() ​

mojo
print(repr(Decimal("123.45")))  # BigDecimal("123.45")

Decimal numeric conversions ​

mojo
var n = Int(Decimal("123.99").truncate())  # 123; Int() alone raises on a fraction
var f = Float64(Decimal("3.14"))   # 3.14 (may lose precision)

Decimal Query Methods ​

MethodReturnDescription
x.is_zero()BoolTrue if value is zero
x.is_one()BoolTrue if value is exactly 1
x.is_integer()BoolTrue if no fractional part
x.is_negative()BoolTrue if negative
x.is_positive()BoolTrue if strictly positive
x.is_odd()BoolTrue if odd integer
x.number_of_digits()IntTotal digits in coefficient
x.adjusted()IntAdjusted exponent (≈ floor(log10(|x|)))
x.same_quantum(y)BoolTrue if both have same scale

as_tuple() — Python-compatible decomposition ​

mojo
var parts = Decimal("7.25").as_tuple()
# parts[0] = False (sign), parts[1] = [7, 2, 5] (digits), parts[2] = -2 (exponent)
# sign=False, digits=[7, 2, 5], exp=-2

Other methods ​

mojo
x.copy_abs()             # Copy with positive sign
x.copy_negate()          # Copy with inverted sign
x.copy_sign(other)       # Copy of x with sign of other
x.fma(a, b)              # Fused multiply-add: x*a+b (exact)
x.scaleb(n)              # Multiply by 10^n (O(1), adjusts scale only)

Python Interoperability ​

From Python ​

mojo
from std.python import Python

var decimal = Python.import_module("decimal")
var py_val = decimal.Decimal("3.14159265358979323846")

var d = Decimal.from_python_decimal(py_val)
# Or:
var d = Decimal(py=py_val)

Matching Python's API ​

Many methods mirror Python's decimal.Decimal API:

Python Decimal methodDecimo equivalent
d.quantize(exp)x.quantize(exp)
d.normalize()x.normalize()
d.as_tuple()x.as_tuple()
d.copy_abs()x.copy_abs()
d.copy_negate()x.copy_negate()
d.copy_sign(other)x.copy_sign(other)
d.fma(a, b)x.fma(a, b)
d.adjusted()x.adjusted()
d.same_quantum(other)x.same_quantum(other)

A note on result exponents (Decimal and Dec128) ​

decimo follows the preferred-exponent rules from IEEE 754-2008 §3.3 and the IBM General Decimal Arithmetic specification §4.1. Concretely:

  • For add/subtract, the result exponent is min(exp(a), exp(b)).
  • For multiply, the result exponent is exp(a) + exp(b) — even when the numerical value is zero. So Decimal("123.45") * Decimal("0") prints as "0.00", not "0", because both operands' scales are preserved in the product's scale (-2 + 0 = -2).
  • For divide, the ideal exponent is exp(a) - exp(b). The result exponent is the ideal one when the quotient is exact at that scale (Decimal("10.5") / Decimal("2.5") prints as "4.2", not "4.20"), and otherwise the smallest exponent that still represents the exact value (Decimal("123.45") / Decimal("-2") prints as "-61.725").
  • The sign of zero is preserved (Decimal("-0.00") round-trips to "-0.00").

This matches Python's decimal.Decimal, System.Decimal (.NET BCL, both C# and VB.NET), and the GDA reference. It differs from rust_decimal, which always pads multiplication-by-zero to the maximum scale of the operands and pads exact divide quotients with trailing zeros to the operand-scale difference. If you need rust_decimal-style output, call .quantize(...) or .normalize() explicitly.

Chinese Numerals ​

Both BInt and Decimal can write themselves in Chinese numerals with to_chinese():

mojo
print(BInt(15).to_chinese())                    # 十五
print(Decimal("1050.07").to_chinese())          # 一千零五十点零七
print(Decimal("-100000001").to_chinese())       # 负一亿零一

The conversion works on the decimal string of the number rather than on a particular numeric type, so it is not limited by any integer width. The string-level entry point is available directly if you need it:

mojo
from decimo.numerals import decimal_string_to_chinese

print(decimal_string_to_chinese("1050.07"))     # 一千零五十点零七

Reading rules ​

The integer part is split into sections of eight digits, counted from the decimal point. Within a section the digits take the 十/百/千 units, and the upper four take 万. Sections are joined by 亿, which multiplies everything read before it rather than labelling one section:

mojo
print(BInt("1234567890123").to_chinese())
# 一万二千三百四十五亿六千七百八十九万零一百二十三  (12345亿 + 6789万 + 123)

Because 亿 is a multiplier, each further 亿 raises the magnitude by another 10^8 — 亿亿 is 10^16, 亿亿亿 is 10^24 — so arbitrarily large integers are readable without the rarely-agreed-upon 兆/京/垓 units:

mojo
print(BInt("10000000000000000").to_chinese())   # 一亿亿
print(BInt("10000000000000005").to_chinese())   # 一亿亿零五

Runs of zeros collapse into a single 零, and a leading 一十 is shortened to 十 (BInt(15) is 十五, not 一十五). The fractional part is read digit by digit after 点, zeros included, so the written precision survives the conversion:

mojo
print(Decimal("1.50").to_chinese())             # 一点五零 (not 一点五)
print(Decimal("2.000").to_chinese())            # 二点零零零

Styles ​

The character tables are supplied by ChineseNumeralStyle, which ships with four presets:

PresetDigitsUnits10^4 / 10^8PointExample for 1050
simplified() (default)一二三十百千万 / 亿点一千零五十
simplified_financial()壹贰叁拾佰仟万 / 亿点壹仟零伍拾
traditional()一二三十百千萬 / 億點一千零五十
traditional_financial()壹貳參拾佰仟萬 / 億點壹仟零伍拾
mojo
from decimo import ChineseNumeralStyle

print(Decimal("1050.07").to_chinese(ChineseNumeralStyle.simplified_financial()))
# 壹仟零伍拾点零柒
print(Decimal("1050.07").to_chinese(ChineseNumeralStyle.traditional()))
# 一千零五十點零七

The financial (大写) styles keep the leading 一十 rather than shortening it, since dropping a digit on a cheque would make the amount easier to tamper with. A custom style can be built from the same constructor if you need a table the presets do not cover.

Digit budget ​

A reading is always written out in full, so its cost is set by the written length of the number, not by how compactly the value was given — Decimal("1E+1000000000") is a few characters of input and a billion digits of output. Conversions therefore take a max_digits budget, defaulting to MAX_CHINESE_NUMERAL_DIGITS (10 000), and raise a ValueError past it. The check happens before the digits are expanded, so an absurd magnitude costs nothing:

mojo
_ = Decimal("1E+1000000000").to_chinese()          # raises ValueError
_ = BInt("1" + String("0") * 20000).to_chinese()   # raises ValueError

# Raise or lift the cap when a very long reading really is what you want:
print(Decimal("1E+20000").to_chinese(max_digits=0))

Expression Engine ​

eval() takes an arithmetic expression as a string and works it out with Decimal arithmetic. It is the same engine that drives the decimo CLI calculator:

mojo
from decimo import eval

print(eval("100 + e * pi"))
# 108.53973422267356706546355086954657449503488853577
print(eval("sqrt(2) + 1/3", precision=30))
# 1.74754689570642838213502205754

The result is rounded to precision significant digits, 50 by default, with rounding_mode, half-even by default:

mojo
print(eval("1/3", precision=20))
# 0.33333333333333333333
print(eval("1/3", precision=5, rounding_mode=ROUND_CEILING))
# 0.33334

What you can write ​

The operators are +, -, *, /, ^ (power), a unary minus or plus, and parentheses. A unary minus binds looser than ^, as in Python: -2^2 is -4. The constants are pi and e. The functions are sqrt, cbrt, root(x, n), ln, log(x, base), log10, exp, sin, cos, tan, cot, csc, and abs.

Names are case-sensitive: pi is the constant, PI is an unknown identifier. (The CLI lower-cases each line before it reaches the engine, which is why PI works there but not here.) Line breaks count as whitespace, so an expression can span several lines:

mojo
print(eval("""
    100 + 2 * 3
"""))                                           # 106

Variables ​

Pass a Dict and the expression can refer to your own values. Any identifier that is not a built-in constant or function is looked up there:

mojo
from std.collections import Dict

var vars = Dict[String, Decimal]()
vars["x"] = Decimal.from_string("10")
vars["y"] = Decimal.from_string("3")
print(eval("x^2 + y", variables=vars))          # 103

Errors ​

A syntax error, an unknown name, a division by zero, or a domain error raises, and the message says where in the expression it went wrong:

mojo
try:
    _ = eval("1 / 0")
except e:
    print(e)      # Error at position 2: division by zero

The individual stages ​

eval() tokenizes, parses to reverse Polish notation, and evaluates. The three stages are exported as well, in case you want to look at the tokens or the RPN form:

mojo
from decimo.expression import tokenize, parse_to_rpn, evaluate_rpn

var rpn = parse_to_rpn(tokenize("1 + 2 * 3"))
print(evaluate_rpn(rpn^, precision=50))         # 7

evaluate() is an alias of eval().

Part III — BigFloat (BFlt) ​

Overview — BigFloat ​

BigFloat is an arbitrary-precision binary floating-point type, written in Mojo with nothing to install. Where Decimal keeps digits and a scale, BigFloat keeps a significand and a power of two, which is what makes it the right type for scientific work: a square root, an exponential or a sine has no exact decimal answer, so the question is not whether it rounds but whether it rounds correctly. Every function here does.

PropertyValue
NameBigFloat
AliasBFlt
Value(-1)^sign * significand * 2^exponent
SignificandA BigInt, exactly precision bits when non-zero
ExponentAn Int, so from Int.MIN to Int.MAX
PrecisionA field on the value, in bits, chosen per call
Special valuesNaN, +Infinity, -Infinity, +0 and -0
RoundingAll seven of decimo's modes, correctly
DependenciesNone

The precision is a field on the value rather than a global context. Two values of different precisions can be added, and the destination's precision is an argument to the operation, not a setting somewhere else:

mojo
from decimo import BigFloat
from decimo.bigfloat.arithmetics import add

var a = BigFloat.from_string("0.1", 53)      # 53 bits
var b = BigFloat.from_string("0.1", 200)     # 200 bits
print(a == b)                                 # False: b holds more of a tenth
print(add(a, b, 300).precision)               # 300: the caller says
print((a + b).precision)                      # 200: an operator takes the wider

Construction — BigFloat ​

mojo
from decimo import BigFloat
from decimo.bigdecimal.bigdecimal import BigDecimal

var from_text = BigFloat.from_string("3.14159", 120)   # rounds, 120 bits
var from_int = BigFloat.from_int(42)                   # exact where it fits
var from_double = BigFloat.from_float64(0.1)           # exact: a double is one
var from_decimal = BigFloat.from_bigdecimal(BigDecimal("0.1"), 53)

var zero = BigFloat.zero(53, False)                    # a signed zero
var infinity = BigFloat.infinity(53, True)             # -Infinity
var nan = BigFloat.nan(53)

from_float64 is exact in both directions where there is room for it: a Float64 is a binary float of 53 bits, so at 53 bits or more nothing is lost either way and every double round-trips bit for bit. Two things qualify that. Asking for fewer bits rounds, like any other conversion -- from_float64(0.1, 24) is not 0.1. And a NaN does not survive: this type keeps one canonical NaN, so a sign or a payload goes in and does not come out.

from_string and from_bigdecimal round, because almost no decimal is a binary float, and they round correctly in whichever mode is asked for.

How Rounding Works — BigFloat ​

Every function takes a precision and a rounding mode, and returns the float of that precision nearest the true value, with the mode settling which side when the true value sits between two. That is a stronger promise than "accurate to the last bit or so", and the difference shows in the directed modes:

mojo
from decimo.bigfloat.exponential import sqrt
from decimo import RoundingMode

var two = BigFloat.from_int(2, 53)
print(sqrt(two, 53, RoundingMode.ROUND_DOWN))      # toward zero
print(sqrt(two, 53, RoundingMode.ROUND_UP))        # away from zero
print(sqrt(two, 53, RoundingMode.ROUND_HALF_EVEN)) # the default

For a transcendental function the true value cannot be computed, only approached, so the answer is decided rather than assumed: the function is evaluated wider than asked, and the rounding is only returned when every value the error allows rounds the same way. If they do not, the width grows and it is tried again. This is Ziv's method, and it is why the last bit can be trusted.

Mathematical Functions — BigFloat ​

FunctionWhere
add, subtract, multiply, dividedecimo.bigfloat.arithmetics
sqrt, exp, lndecimo.bigfloat.exponential
root, cbrtdecimo.bigfloat.exponential
hypotdecimo.bigfloat.exponential
exp2, exp10, log2, log10, logdecimo.bigfloat.exponential
expm1, log1pdecimo.bigfloat.exponential
powerdecimo.bigfloat.power
sin, cos, tandecimo.bigfloat.trigonometric
cot, csc, secdecimo.bigfloat.trigonometric
arcsin, arccos, arctandecimo.bigfloat.trigonometric
arctan2decimo.bigfloat.trigonometric
sinh, cosh, tanhdecimo.bigfloat.hyperbolic
arcsinh, arccosh, arctanhdecimo.bigfloat.hyperbolic
pi, ln2, ln10, edecimo.bigfloat.constants
fma, remainder, fmoddecimo.bigfloat.ieee
mojo
from decimo.bigfloat.constants import pi
from decimo.bigfloat.exponential import exp, ln
from decimo.bigfloat.trigonometric import sin

print(pi(200).to_bigdecimal_rounded(60))
print(exp(BigFloat.from_int(1), 120))
print(ln(BigFloat.from_int(2), 120))
print(sin(BigFloat.from_string("1e10", 60), 53))

That last one is worth a word. Reducing an argument of 10^10 by pi/2 has to cancel its leading bits against pi, so the number of bits of pi needed is counted from the argument rather than fixed — sin(2^300) fetches three hundred bits of pi before the first bit of its answer is right. An argument whose leading bit sits beyond what a precision may be is refused rather than answered badly.

Each function also has a method, which asks for this value's own precision and the default rounding; the free function is where the precision and the mode live:

mojo
print(BigFloat.from_int(8, 53).log2())        # 3
print(BigFloat.from_int(10, 53).log10())      # 1
print(BigFloat.from_int(27, 53).log(BigFloat.from_int(3, 53)))  # 3

The Other Bases, and the Exact Answers — BigFloat ​

log2, log10, log, exp2 and exp10 have something the natural exponential and logarithm do not: arguments whose answers a float holds exactly. log2(8) is three, and three is a float.

That matters because a correctly rounded transcendental function works by deciding its rounding rather than assuming it — evaluating wider than asked until every value the error allows rounds the same way. An answer sitting exactly on a representable value can never be decided that way, however wide the evaluation goes, because there are values on both sides of it that round differently. So those arguments are recognized first, from the integer significand, and answered exactly:

mojo
from decimo.bigfloat.exponential import exp2, log, log10, log2

print(log2(BigFloat.power_of_two(-70), 53))        # exactly -70
print(log10(BigFloat.from_int(1000), 53))          # exactly 3
print(log(BigFloat.from_int(27), BigFloat.from_int(3), 53))   # exactly 3
print(log(BigFloat.from_int(3), BigFloat.from_int(9), 53))    # exactly 0.5
print(exp2(BigFloat.from_int(10), 53))             # exactly 1024

Which arguments those are has an exact answer in each case, and each is worth knowing:

  • log2(x) is exact exactly when x is a power of two, which is every argument whose significand is a single bit, at any exponent.
  • log10(x) is exact exactly when x is 10^k for a k at nought or above. A negative power of ten is not a binary float at all — a fifth is not a dyadic rational — so log10 of the float nearest a thousandth is irrational and a hair away from -3, and it is answered by the series.
  • log(x, base) is exact whenever the answer is exactly representable, which includes log(3, 9) = 0.5 and not only the whole answers. An answer that is exact but not representable, like log(9, 27) = 2/3, goes through the series and comes back correctly rounded like any other.
  • exp2(x) is exact exactly when x is a whole number, and exp10(x) when x is a whole number at nought or above and 10^x still fits the precision asked for. Past that 10^x is not representable, so the series settles on it.
  • expm1 and log1p are exact only at zero, where both are zero with the sign of the argument, and expm1(-Infinity) which is exactly minus one.

A Point in the Plane — BigFloat ​

hypot(x, y, precision, mode) is the length of the point and arctan2(y, x, precision, mode) is its angle, with x.hypot(y) and y.arctan2(x) over them. Together they turn a pair of coordinates into polar form.

hypot forms x*x + y*y exactly, in integers, and never as two roundings that could each be wrong. Both squares are integers — a significand is one — so the sum over their common exponent is an integer too, and the integer square root of it brackets the answer with its remainder as the sticky bit. So hypot(3, 4) is 5, in every mode, which a pair of rounded squares and a rounded root could not promise.

A leg far below the other contributes nothing but a sticky bit, and the answer is then the larger leg moved by a hair — not the larger leg itself, which is the mistake that would make hypot(x, tiny) wrong in the directed modes.

arctan2 is where signed zeros earn their keep. arctan2(0, -1) is pi and arctan2(-0, -1) is -pi: the two zeros lie on opposite shores of the cut along the negative axis, and the sign is the only thing that says which one a point on the cut came from. The argument order is the one every other language uses — the second coordinate first, because the function inverts a tangent and a tangent is a rise over a run.

mojo
from decimo.bigfloat import arctan2, hypot
from decimo.bigfloat.bigfloat import BigFloat

def main() raises:
    var x = BigFloat.from_int(3, 53)
    var y = BigFloat.from_int(4, 53)
    print(hypot(x, y, 53))            # 5
    print(arctan2(y, x, 53))          # 0.92729521800161219

Roots of Any Degree — BigFloat ​

root(x, degree, precision, mode) and cbrt(x, precision, mode), also x.root(n) and x.cbrt(), take any positive degree.

They need no series and no deciding loop, because a root is algebraic. The integer degree-th root of a scaled significand brackets the true root between two consecutive integers, and whether it landed on the lower one exactly is the sticky bit; one rounding then finishes the job in each of the seven modes. So an exact root comes out exact — root(8, 3) is 2 in every mode, including the directed ones, where a function that evaluated and then rounded would have to pick a side and both sides would be wrong.

Dividing the exponent is the whole of the scaling. x is s * 2^(q * degree + r) with r below the degree, so the factor of 2^r moves into the significand and the root sits at 2^q. The product is never formed again, which is what keeps a value near the bottom of the exponent range from leaving it.

A negative value has a real root only at an odd degree, and gives a NaN at an even one, the way sqrt does. A nought keeps its sign at an odd degree and loses it at an even one. A degree of nought or less is refused rather than answered.

The Power — BigFloat ​

power(x, y, precision, mode), also x.power(y) and x ** y, raises one value to another. It ends in exp(y ln x), and almost nothing about it is that line.

The reason is the same one the other bases have, only more so. A power sits on an exactly representable answer constantly: 2 ** 10 is 1024, which every directed mode has to return and no bracket around it can prove. What is less obvious is that a fractional exponent does it too — 4 ** 0.5 is exactly 2, and 16 ** 0.25 is as well — so recognizing the whole exponents is not enough.

Three kinds of exact answer are found before any series runs.

A whole exponent is a power of the significand. x is s * 2^e, so x^n is s^n * 2^(en), and s^n is an integer however large; it is formed while it is short enough to be worth forming, and one rounding finishes it. A negative whole exponent is one correctly rounded division of exact operands.

A base that is a power of two answers any exponent leaving a whole result: 2^m to the a / 2^b is 2^(ma / 2^b), exact exactly when 2^b divides m.

Anything else with a fractional exponent — and every binary float is a / 2^b for some a and b — is b square roots followed by a whole power. A square root is exact only when the significand is a perfect square, and each exact root halves the bits, so the question answers itself within a few steps or not at all.

What reaches the series is therefore known to be irrational, where the loop settles.

mojo
from decimo.bigfloat import power
from decimo.bigfloat.bigfloat import BigFloat
from decimo.rounding_mode import RoundingMode

def main() raises:
    var two = BigFloat.from_int(2, 53)
    print(two ** BigFloat.from_int(10, 53))      # 1024, in every mode
    print(BigFloat.from_int(4, 53) ** BigFloat.from_string("0.5", 53))  # 2
    print(power(two, BigFloat.from_string("0.5", 53), 53,
                RoundingMode.ROUND_DOWN))        # 1.4142135623730949

The special values follow IEEE 754's pow, which answers two cases before it looks at anything else: a nought exponent gives one for every base, a NaN included, and a base of one gives one for every exponent, a NaN included. A negative base with a fractional exponent gives a NaN, there being no real answer. There is no overflow to an infinity for finite arguments — this type's exponent is as wide as an Int, and an answer asking for more is refused rather than answered with an infinity it did not earn.

Holding On Near Zero — BigFloat ​

expm1 is exp(x) - 1 and log1p is ln(1 + x), and both exist because the expressions they are named after throw their answers away for a small argument. exp(2^-100) rounds to one, so the difference is nought where the answer is 2^-100; 1 + 2^-100 rounds to one, so the logarithm is nought too. These two compute the difference and the logarithm directly:

mojo
from decimo.bigfloat.exponential import expm1, log1p

var tiny = BigFloat.power_of_two(-100000)
print(expm1(tiny, 53).internal_representation())   # 2^-100000, not nought
print(log1p(tiny, 53).internal_representation())   # and a hair under it

They are also what the hyperbolic functions are built on, which is why sinh(2^-100000) is 2^-100000 rather than zero.

Special Values — BigFloat ​

The special values follow IEEE 754 rather than Decimal's rules, because this is a float:

mojo
from decimo.bigfloat.trigonometric import arctan

print(sqrt(BigFloat.from_int(-4), 53).is_nan())      # True, not an error
print(ln(BigFloat.zero(), 53))                        # -Infinity
print(sin(BigFloat.infinity(), 53).is_nan())          # True: it settles nowhere
print(arctan(BigFloat.infinity(), 53))                # pi/2: this one settles

A NaN is unordered, so every comparison with it is False except !=, and that holds for a NaN against itself. The two zeros compare equal. Sorting therefore needs an order that covers them, and compare_total() is it: it runs from -Infinity up to the NaN and separates values that are numerically equal by their precision.

There is no overflow to an infinity for a finite argument. The exponent is as wide as an Int, so an operation that asks for more — exp of anything from about 6.4e18 up — raises rather than returning an infinity it did not earn.

The IEEE 754 Companions — BigFloat ​

Beyond the arithmetic and the functions, IEEE 754 asks a format for a set of small operations that numerical code reaches for constantly. They are in decimo.bigfloat.ieee, which is where they are imported from, and each one is also a method on the value:

FunctionWhat it gives
next_plus(x, p)The smallest value above x at p bits
next_minus(x, p)The largest value below it
next_toward(x, y, p)One step from x in the direction of y
logb(x)floor(log2(abs(x))), as a BigInt
scaleb(x, n)x * 2^n, exactly
copy_sign(x, y)x with the sign of y
copy_abs(x), copy_negate(x)The sign dropped, the sign flipped
number_class(x)+Normal, -Zero, NaN, and so on
is_integer(x)Whether x is a whole number
truncate(x), floor(x), ceil(x)The three named roundings to a whole number
round_to_integer(x, mode)The same in any of the seven modes
fma(x, y, z, p, mode)x * y + z with a single rounding
remainder(x, y, p)x - y*n, n the nearest integer
fmod(x, y, p)x - y*n, n the quotient truncated
mojo
from decimo import BigFloat
from decimo.bigfloat.ieee import fma, next_plus, remainder

var one = BigFloat.from_float64(1.0)
print(next_plus(one, 53))                     # 1 + 2^-52
print(one.next_minus(53))                     # 1 - 2^-53, half a step away
print(String(BigFloat.from_float64(12.0).logb()))   # 3
print(one.scaleb(10))                          # 1024, nothing computed
print(BigFloat.from_string("-0.5", 53).ceil()) # -0, the sign surviving
print(remainder(BigFloat.from_float64(5.0), BigFloat.from_float64(3.0), 53))
# -1: six is nearer to five than three is. `fmod` of the same pair is 2.

fma is the one with substance. x * y + z written out rounds the product before the addition sees it, and a product that nearly cancels with z loses exactly the bits the answer is made of. Here the product is formed exactly — it is an integer times a power of two, so there is nothing to round — and only the sum is rounded:

mojo
var above = next_plus(one, 53)             # 1 + 2^-52
var product = above * above                 # rounded: 1 + 2^-51
print(product + (-product))                 # 0: the two-step answer
print(fma(above, above, -product, 53))      # 2^-104: what the rounding dropped

Two of these differ from a double, and both follow from the type rather than from a choice. number_class never answers +Subnormal or -Subnormal: a significand here always holds its full precision, so every finite non-zero value is normal. And next_plus and next_minus step through an exponent range as wide as an Int, so the ends they reach are not a double's — next_plus(BigFloat.zero(), 53) is 2^(Int.MIN + 52) where a double answers 2^-1074. Within that range the two agree bit for bit, which is how these are tested.

logb returns a BigInt rather than an Int because the position of the leading bit is exponent + precision - 1, and that sum can be outside an Int while the value itself is an ordinary one. It has no answer for a zero, an infinity or a NaN, where the standard's answers are infinities and a NaN, and raises instead — as the decimal logb() does.

Decimal In, Decimal Out — BigFloat ​

Every binary float is exactly a decimal, because 2^-k is 5^k / 10^k, so the conversion out loses nothing and only the number of digits is a choice:

mojo
var tenth = BigFloat.from_float64(0.1)
print(tenth.to_bigdecimal())            # all 55 digits of the double
print(tenth.to_bigdecimal_rounded(17))  # 0.10000000000000001
print(tenth)                            # as many digits as 53 bits are worth
print(tenth.internal_representation())  # the significand, exponent, precision

A digit count costs what the digits cost. The digits asked for are taken by scaling the value by a power of ten, so a deep exponent is no longer a reason to wait: a value whose exact expansion runs to 301,031 digits still gives twenty of them at once, and the smallest and largest values a BigFloat can hold print like any other.

mojo
var deep = BigFloat.power_of_two(-1000000)
print(deep.to_string(20))               # 1.0100340591980302247E-301030

to_bigdecimal() is the one to ask when every digit is wanted, and it is the only one whose cost follows the exponent, because the answer does.

Coming in costs what the precision is, in the same way and for the same reason. A decimal exponent of nine or eighteen figures is a decimal nobody could write out, and it parses at once:

mojo
var deep = BigFloat.from_string("1.5e-1000000", 53)
print(deep.to_string(17))                   # 1.5000000000000000E-1000000
print(BigFloat.from_string("7e1000000000", 53).to_string(17))
# 6.9999999999999999E+1000000000

The second one is seven to a billion powers of ten, which has no exact rendering in 53 bits -- the digits shown are the float's own.

Going the other way rounds. to_float64() is exact for a normal double but rounds a second time when the answer is subnormal, since a subnormal has fewer bits than 53: converting from a wider precision avoids it.

BigFloat, Decimal, or MPF ​

Use Decimal when the arithmetic has to be decimal — money, anything whose digits are the point, anything that has to agree with Python's decimal.Decimal. Use BigFloat for scientific computation at a chosen precision, where binary is the natural base and the question is whether the last bit is right.

MPF, the MPFR-backed float that used to carry the name BigFloat, is still in the source at decimo.mpf.mpf.MPF. It is not exported and carries no promise of a stable interface, and using it means installing MPFR and linking a C wrapper. It is kept as a second implementation to test the traits against, as the baseline the Mojo one is measured against, and for anyone who was relying on it.

Part IV — Rational ​

An exact fraction of two arbitrary-precision integers. Nothing is ever rounded: a third is a third, and a third times three is one.

Rational is not in the prelude, which carries the four types most programs reach for. Import it by name, as itself or under the short name Frac:

mojo
from decimo import Frac, Rational
from decimo.bigint.bigint import BigInt

The value is always in lowest terms with a positive denominator, so two equal values have the same two parts and == is a comparison of integers rather than of fractions.

Construction — Rational ​

mojo
print(Rational(3, 4))                      # 3/4
print(Rational(BigInt(6), BigInt(8)))      # 3/4, reduced on the way in
print(Rational("22/7"))                    # 22/7
print(Rational("0.25"))                    # 1/4, a decimal literal
print(Rational.from_float_scalar(0.1))     # 3602879701896397/36028797018963968

The float constructor is exact and that is the point of it: a double holding "0.1" is not a tenth, and this is the fraction it really holds. Use Rational("0.1") for the tenth.

Arithmetic — Rational ​

The four operations, the comparisons and the in-place forms are all exact. Multiplication and division reduce across the two fractions before multiplying, so the intermediates stay near the size of the answer rather than growing to the product of the denominators.

mojo
print(Rational(1, 2) + Rational(1, 3))     # 5/6
print(Rational(1, 2) / Rational(1, 3))     # 3/2
print(Rational(1, 3) < Rational(1, 2))     # True

var x = Rational(1, 2)
x += Rational(1, 3)                        # 5/6
x **= 3                                    # 125/216

A power takes a whole exponent, which may be negative:

mojo
print(Rational(3, 2) ** 7)                 # 2187/128
print(Rational(3, 2) ** -2)                # 4/9

A fractional exponent is not accepted. Python's Fraction answers one with a float, which throws away the exactness the type exists for; ask BigFloat for that answer instead, where the precision is yours to choose.

Rounding — Rational ​

The four roundings all return a Rational with a denominator of one, and the standard library's functions take this type:

mojo
from std.math import ceil, floor, trunc

var half_down = Rational(-7, 2)
print(floor(half_down))                    # -4
print(ceil(half_down))                     # -3
print(trunc(half_down))                    # -3, toward zero
print(round(half_down))                    # -4
print(round(Rational(5, 2)))               # 2, halves go to the even side
print(round(Rational(1, 8), 2))            # 3/25, which is 0.12

round(x, n) takes a negative n to round above the decimal point, as Python's does: round(Rational(1350), -2) is 1400.

The Closest Fraction Under a Limit — Rational ​

limit_denominator answers the closest fraction whose denominator is at most the limit, which is how a long exact value becomes a short readable one:

mojo
var pi_ish = Rational(BigInt(31415926535), BigInt(10000000000))
print(pi_ish.limit_denominator(BigInt(10)))     # 22/7
print(pi_ish.limit_denominator(BigInt(1000)))   # 355/113

It walks the continued fraction of the value, so the work is proportional to the length of that walk and not to the size of the denominator it starts from. Where two fractions are equally close it returns the same one Python's Fraction does, which is not always the one with the smaller denominator.

Conversions — Rational ​

mojo
print(Rational(1, 3).to_float())           # 0.3333333333333333
print(Rational(1, 3).to_bigdecimal(20))    # 0.33333333333333333333
print(Rational(-7, 2).to_integer())        # -3, truncated toward zero
print(Rational(-7, 2).reciprocal())        # -2/7

to_bigdecimal takes the digits to keep, because a third has no decimal expansion that ends. Everything else here is exact.

Appendix A — Import Paths ​

mojo
# Recommended: import everything commonly needed
from decimo.prelude import *
# Brings in: BigInt, BInt, Decimal, BigDecimal, BDec, Decimal128, Dec128,
#   BigFloat, BFlt, RoundingMode, ROUND_DOWN, ROUND_HALF_UP,
#   ROUND_HALF_EVEN, ROUND_UP, ROUND_CEILING, ROUND_FLOOR

# Or import specific types
from decimo import BInt, BigInt
from decimo import Decimal  # also available as BigDecimal or BDec
from decimo import BigFloat, BFlt
from decimo import RoundingMode

# The exact fraction is exported but not in the prelude, which carries the
# four types most programs reach for.
from decimo import Rational

# The MPFR-backed float is in the source but not exported, and carries no
# promise of a stable interface. It also needs its C wrapper linked; see
# `src/decimo/mpf/__init__.mojo`.
from decimo.mpf.mpf import MPF

# Number-theory free functions
from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse

# The binary float's own functions live in their modules; the top
# level exports the type and not its functions
from decimo.bigfloat import exp, exp2, exp10, expm1, ln, ln10, log
from decimo.bigfloat import log1p, log2, log10, sqrt
from decimo.bigfloat import power
from decimo.bigfloat import cbrt, root
from decimo.bigfloat import arctan2, hypot
from decimo.bigfloat import ceil, floor, number_class, truncate
from decimo.bigfloat import fma, logb, next_plus, remainder, scaleb
from decimo.bigfloat.trigonometric import cos, sin, tan

# Chinese numerals: the style presets are re-exported at the top level, the
# string-level engine lives in the `decimo.numerals` sub-package
from decimo import ChineseNumeralStyle
from decimo.numerals import (
    decimal_string_to_chinese,
    MAX_CHINESE_NUMERAL_DIGITS,
)

# Expression engine: `eval` at the top level, the individual stages in the
# `decimo.expression` sub-package
from decimo import eval
from decimo.expression import tokenize, parse_to_rpn, evaluate_rpn

Appendix B — Traits Implemented ​

All of these come from the Mojo standard library except Numeric, Parsable and Rootable, which Decimo defines in decimo.traits. BigInt, BigDecimal and Decimal128 conform to all three. Mojo checks conformance nominally, which is why the traits live in Decimo rather than in the consumer.

Numeric gathers zero(), one(), -x, +, -, * and /, so a generic routine — a matrix or polynomial library, say — can be written once against T: Numeric and run over any of the three.

Parsable requires the static from_string(value), so the same generic routine can also fill itself from text. The two are separate because the capabilities are: the MPFR-backed MPF, which is in the source but outside the public interface, parses but is Movable without being Copyable, so it can never be Numeric. Ask for T: Numeric & Parsable to get both.

Rootable requires sqrt(), the one operation a Cholesky or QR factorisation needs beyond arithmetic. It is separate for the same reason, and here the evidence is sharper: BigUInt has a square root but is unsigned, so it has no __neg__ and can never be Numeric either. Its supertraits are Deinitable and Movable and no more — Copyable is pointedly absent, so that MPF can conform. BigFloat, MPF and BigUInt therefore conform to Rootable as well, six types in all. Ask for T: Numeric & Rootable in a routine that needs both. What the root means stays the implementing type's business: on an integral type it truncates, so BigInt("10").sqrt() is 3, exactly as / truncates there. So does what a negative value does — the four exact types raise, while BigFloat and MPF return a NaN, as they do for every other function outside their domain.

Hashable gives hash(x), and with it a key in a Dict. BigUInt, BigInt, BigDecimal, BigFloat, Rational and Decimal128 all conform. The rule a hash has to keep is that equal values hash alike, and each of these types holds values that are equal without being written the same way, so each has something to see through. A decimal drops the trailing zeros it was written with, which is why 1.0 and 1.00 reach the same entry. A binary float shifts its significand down to an odd number, which is why a half at fifty-three bits and a half at five hundred reach the same entry. A zero is one key whatever sign it carries. The converse is not promised: two different numbers may land on the same sixty-four bits, as they may under any hash, and the dictionary settles that with ==.

Every one of these types also answers String(x) and repr(x). Neither needs a trait of its own here: String(x) and print(x) come from Writable, and repr(x) from the type's own __repr__. Mojo 1.1 has no Stringable or Representable to ask for, so generic code that wants to print its argument asks for Writable.

BigInt ​

TraitWhat it enables
Absableabs(x)
Comparable<, <=, >, >=, ==, !=
CopyableValue-semantic copy
MovableMove semantics
FloatableRaisingFloat64(x)
Hashablehash(x), a key in a dictionary
IntableRaisingInt(x)
NumericGeneric code over Decimo numbers
ParsableT.from_string(text) in generic code
Rootablex.sqrt() in generic code
Writableprint(x), String(x)

BigDecimal ​

TraitWhat it enables
Absableabs(x)
Comparable<, <=, >, >=, ==, !=
CopyableValue-semantic copy
MovableMove semantics
FloatableRaisingFloat64(x)
Hashablehash(x), a key in a dictionary
IntableRaisingInt(x)
NumericGeneric code over Decimo numbers
ParsableT.from_string(text) in generic code
Rootablex.sqrt() in generic code
Roundableround(x), round(x, ndigits)
Writableprint(x), String(x)

Decimal128 ​

TraitWhat it enables
Absableabs(x)
BoolableBool(x), if x
Comparable<, <=, >, >=, ==, !=
FloatableFloat64(x), which cannot overflow
Hashablehash(x), a key in a dictionary
IntableRaisingInt(x)
NumericGeneric code over Decimo numbers
ParsableT.from_string(text) in generic code
Rootablex.sqrt() in generic code
Roundableround(x), round(x, ndigits)
TrivialRegisterPassablePassed in registers, copied bitwise
Writableprint(x), String(x)

BigFloat ​

TraitWhat it enables
Absableabs(x)
Comparable<, <=, >, >=, ==, !=
CopyableValue-semantic copy
Hashablehash(x), a key in a dictionary
MovableMove semantics
ParsableT.from_string(text) in generic code
Rootablex.sqrt() in generic code
Writableprint(x), String(x)

Parsable asks for a from_string(text) of one argument, and a binary float has to be told how many bits to keep, so there are two: the one the trait needs, which lands at the default precision of fifty-three bits, and from_string(text, precision, rounding_mode) for everything else. Generic code bounded on T: Parsable therefore gets a float at a double's precision, which is the only precision it could have asked for without saying so.

Comparable is conformed to with the one reservation IEEE 754 asks for: a NaN is unordered, so <, <=, >, >= and == are all False for it and != is True, which breaks the law that exactly one of less, equal and greater holds. compare_total() is there for code that needs a total order.

Numeric is absent for now: it asks for operators that round, and this type's operators have to be told a precision to round to. The free functions in decimo.bigfloat.arithmetics take one.

Appendix C — Complete API Tables ​

BigFloat — All Functions ​

Every one of these takes a precision in bits and a rounding mode, and returns the float of that precision nearest the true value.

FunctionModule
add, subtractdecimo.bigfloat.arithmetics
multiply, dividedecimo.bigfloat.arithmetics
sqrtdecimo.bigfloat.exponential
root, cbrtdecimo.bigfloat.exponential
hypotdecimo.bigfloat.exponential
exp, lndecimo.bigfloat.exponential
exp2, exp10decimo.bigfloat.exponential
log2, log10, logdecimo.bigfloat.exponential
expm1, log1pdecimo.bigfloat.exponential
powerdecimo.bigfloat.power
sin, cos, tandecimo.bigfloat.trigonometric
cot, csc, secdecimo.bigfloat.trigonometric
arcsin, arccos, arctandecimo.bigfloat.trigonometric
arctan2decimo.bigfloat.trigonometric
sinh, cosh, tanhdecimo.bigfloat.hyperbolic
arcsinh, arccosh, arctanhdecimo.bigfloat.hyperbolic
pi, ln2, ln10, edecimo.bigfloat.constants
fma, remainder, fmoddecimo.bigfloat.ieee

log takes the base as its second argument, before the precision. Every other one takes the argument, the precision and the mode.

cot and csc have a pole at zero and return an infinity of the zero's sign, the way a float does; BigDecimal raises there instead, having no infinity to return. No other pole of the six is a binary float, so every other argument has an answer, however large it is.

These take no rounding mode, or no precision, because their answers are exact or carry the argument's own precision. All of them are in decimo.bigfloat.ieee, and all of them are methods as well.

FunctionDescription
next_plus, next_minusThe neighbour at a given precision
next_towardThe neighbour in another value's direction
logbThe leading bit's position, as a BigInt
scalebTimes a power of two, exactly
copy_sign, copy_abs, copy_negateThe sign, never raising
number_classThe specification's name for the value
is_integerWhether there is a fractional part
truncate, floor, ceilThe three named roundings to a whole number
round_to_integerThe same in any of the seven modes
Operator / MethodDescription
a + b, a - bAt the wider precision, half to even
a * b, a / bThe same
-a, abs(a)Exact
a == b, a < b, and the restFalse for a NaN, both zeros equal
a.sqrt()At a's own precision, half to even
a.fma(b, c, p)a * b + c, the product formed exactly
a.remainder(b, p), a.fmod(b, p)The two remainders
a.next_plus(p), a.next_minus(p)The neighbours
a.logb(), a.scaleb(n)The exponent, read and written
a.truncate(), a.floor(), a.ceil()Rounded to a whole number
a.round_to_integer(mode)The same in any mode
a.number_class(), a.is_integer()What kind of value it is
a.exp(), a.ln()The same
a.exp2(), a.exp10()The same; a whole exponent is exact
a.log2(), a.log10()The same; a whole power of the base is exact
a.log(base)At the wider precision, half to even
a.expm1(), a.log1p()The same; these keep a small argument
a.to_bigdecimal()The exact decimal expansion
a.to_bigdecimal_rounded(n)n significant digits
a.to_float64()The nearest double
a.internal_representation()The significand, exponent and precision

BigInt — All Operators ​

Operator / MethodAcceptsRaises?Description
a + bBInt, IntNoAddition
a - bBInt, IntNoSubtraction
a * bBInt, IntNoMultiplication
a / bBInt, IntYesTruncating division
a // bBInt, IntYesFloor division
a % bBInt, IntYesFloor modulo
a ** bBInt, IntYesPower
a << nIntNoLeft shift
a >> nIntNoRight shift
a & bBInt, IntNoBitwise AND
a | bBInt, IntNoBitwise OR
a ^ bBInt, IntNoBitwise XOR
~a—NoBitwise NOT
-a—NoNegation
abs(a)—NoAbsolute value
a.sqrt()—YesInteger square root
a.gcd(b)BIntNoGCD
a.lcm(b)BIntYesLCM
a.extended_gcd(b)BIntYesExtended GCD
a.mod_pow(e, m)BInt/Int, BIntYesModular exponentiation
a.mod_inverse(m)BIntYesModular inverse

Decimal — Mathematical Functions ​

FunctionSignatureDefaultDescription
sqrtx.sqrt(precision=28)28Square root
cbrtx.cbrt(precision=28)28Cube root
rootx.root(n, precision=28)28Nth root
powerx.power(exp, precision=28)28Exponentiation
expx.exp(precision=28)28e^x
lnx.ln(precision=28)28Natural logarithm
logx.log(base, precision=28)28Logarithm (any base)
log10x.log10(precision=28)28Base-10 logarithm
sinx.sin(precision=28)28Sine (radians)
cosx.cos(precision=28)28Cosine (radians)
tanx.tan(precision=28)28Tangent (radians)
cotx.cot(precision=28)28Cotangent (radians)
cscx.csc(precision=28)28Cosecant (radians)
secx.sec(precision=28)28Secant (radians)
arctanx.arctan(precision=28)28Arctangent (radians)
piDecimal.pi(precision)—Compute π
eDecimal.e(precision)—Compute e

Mojo Miji - A Guide to Mojo Programming Language from A Pythonista's Perspective · 魔咒秘籍 - Pythonista 視角下的 Mojo 編程語言指南