Standard library Dodo 0.1.4

Portable mathematics

Checked integers and portable binary64 elementary functions, accuracy, special values, and reference tests.

On this page

Use std/math.checked_add for recoverable signed arithmetic and math.hypot for a vector length. Import std/math/trig separately when you need angles. These portable modules need no allocator or native libm.

The library separates exact integer operations from approximate floating-point operations. A checked integer sum is either exact or an error. An f64 result can be rounded even when the operation succeeds. Choose the numeric type and the error policy before choosing a function.

Input or task Package/API Result style
Byte counts, lengths, target-sized arithmetic core/num Checked usize, plus saturating/wrapping helpers
Signed whole numbers math.checked_* Exact i64 or MathError
GCD, integer square root, powers of two math.gcd, isqrt, checked_next_power_of_two Exact u64; potentially overflowing operations are checked
Roots, logs, exponentials, real powers std/math Approximate f64; invalid domains/finite overflow can be errors
Angles std/math/trig Radians; checked and infallible operations as listed below
Parse or print numbers std/text, std/fmt See parsing and formatting

Quickstart

Save this as math_start.dodo:

package math_start
import "std/math"

fn main() -> i32 {
    match math.checked_add(20, 22) {
        ok(value) => { assert_eq(value, 42i64) },
        err(_) => { return 1 },
    }
    assert(math.abs(math.hypot(3.0, 4.0) - 5.0) < 1e-12)
    return 0
}
dodo run math_start.dodo

Expected output: none; exit 0 confirms the integer sum is 42 and the vector length is approximately 5. Exit 1 means arithmetic overflowed: validate or reduce the input, or report the error. Domain errors from checked elementary functions should likewise be handled, not unwrapped. Floating comparisons need a task-appropriate tolerance; detailed numerical bounds follow below.

API and contracts

std/math supplies allocation-free integer algorithms, checked signed arithmetic, and binary64 (f64) mathematics. std/math/trig separately supplies trigonometry and its range-reduction table. Neither package imports an allocator, clock, OS, libc, libm, or runtime startup. No hardware backend is required or currently provided. Cortex-M0 builds use the target’s ordinary compiler support routines for soft floating-point arithmetic; firmware supplies those when linking.

package vector_example
import "std/math"
import "std/math/trig"

@test
fn reconstructs_the_horizontal_component() {
    length := math.hypot(3.0, 4.0)
    angle := trig.atan2(4.0, 3.0)
    match trig.cos(angle) {
        ok(value) => { assert(math.abs(length * value - 3.0) < 1e-12) },
        err(_) => { assert(false, "a finite vector should have a valid angle") },
    }
}

See examples/math_portable.dodo for an executable example. All checked results must be handled or propagated with ?; math does not bypass Result checking. To run the test-only example above, save it as vector_example.dodo and run dodo test vector_example.dodo. atan2 takes (y, x), so (4, 3) measures the angle of the vector whose horizontal component is 3 and vertical component is 4.

Use angles and tolerances deliberately

Convert degrees to radians with degrees * math.PI / 180.0, and radians to degrees with radians * 180.0 / math.PI. Use atan2(y, x) when deriving an angle from coordinates: it preserves quadrant information that atan(y / x) loses and handles zero coordinates explicitly.

For tests near zero, an absolute tolerance such as math.abs(actual - expected) < 1e-12 can be appropriate. For quantities with a large range of magnitudes, combine absolute and relative tolerances chosen for the application. math.EPSILON describes the spacing near 1.0; it is not a universal error bound for every result or algorithm. Reject non-finite inputs with is_finite when your application requires finite values, since a successful checked operation can still return NaN or infinity under the contracts below.

Integers

checked_abs, checked_add, checked_sub, checked_mul, checked_div, checked_rem, and checked_pow operate on i64. Overflow produces MathError.Overflow; division or remainder by zero produces MathError.DivisionByZero. Unlike the primitive signed-remainder operator, checked_rem(I64_MIN, -1) returns zero. unsigned_abs returns a u64 magnitude, including the signed minimum. checked_pow_u64 accepts an unsigned base. Both power functions take a u32 exponent, use exponentiation by squaring, and define zero to the zeroth power as one.

gcd uses Euclid’s algorithm; gcd(0, 0) is zero. checked_lcm divides before multiplication and returns zero if either input is zero. isqrt is the exact floor of the square root throughout u64, including its maximum; it uses a restoring binary algorithm without floating-point conversion. checked_next_power_of_two maps zero and one to one and reports overflow beyond 2^63. Pointer-sized checked and wrapping resource operations remain available in core/num.

These routines use constant storage. Powers take logarithmic time in the exponent; GCD takes logarithmic iterations in the input magnitudes; integer square root has at most 32 iterations. Checked scalar arithmetic is constant time, without claiming timing independence from its inputs.

Binary64 values and rounding

to_bits and from_bits preserve all 64 representation bits on every target, independent of byte order. classify distinguishes Zero, Subnormal, Normal, Infinite, and Nan; predicates are also available. sign_bit observes negative zero. abs clears the sign bit; copy_sign replaces only the sign bit. Constants include PI, TAU, E, LN_2, LN_10, EPSILON, MIN_NORMAL, and MAX_FINITE. infinity() and nan() construct positive infinity and a quiet NaN.

I64_MIN, I64_MAX, and U64_MAX provide the exact integer boundaries. The API reference lists all constants, classifiers, and functions.

trunc, floor, and ceil round toward zero, negative infinity, and positive infinity. round rounds to nearest with ties away from zero; round_even uses ties to even. Integral rounding is exact, passes through NaNs and infinities, and preserves the sign when its result is zero. No large float-to-integer cast is used. scalbn(x, n) scales by an i32 power of two; it can produce infinity or gradual underflow and preserves signed zero. Scaling is deliberately an IEEE value operation, rather than a checked resource operation.

The implementation assumes IEEE 754 binary64 with round-to-nearest, ties-to-even arithmetic and gradual underflow. Dodo emits arithmetic without fast-math flags. A caller that changes hardware rounding or enables flush-to-zero steps outside the numerical accuracy contract. No floating-point exception flags, signaling NaN behavior, or preservation of NaN payloads through arithmetic are promised. Binary32 functions are not yet supplied; callers may explicitly promote f32 inputs and use the binary64 results.

Rounding operation 2.5 -2.5 Rule
trunc 2.0 -2.0 Toward zero
floor 2.0 -3.0 Toward negative infinity
ceil 3.0 -2.0 Toward positive infinity
round 3.0 -3.0 Nearest, halfway away from zero
round_even 2.0 -2.0 Nearest, halfway to an even integer

These functions still return f64. Rounding a floating-point value does not by itself prove that a later conversion to an integer will fit.

Elementary functions and exceptional inputs

Functions Contract
sqrt Negative values except negative zero are Domain; signed zero and positive infinity pass through.
cbrt Real cube root for either sign; signed zero and infinities pass through.
hypot Scaled Euclidean norm; infinity takes precedence over NaN; overflow yields infinity.
log, log2, log10 Negative arguments are Domain; either zero returns negative infinity; positive infinity passes through.
log1p Stable log of 1+x; below -1 is Domain; exactly -1 returns negative infinity.
exp, exp2, expm1 Finite overflow is Overflow; underflow is gradual and may round to zero; positive infinity gives infinity.
powi i32 exponent; zero to a negative exponent is DivisionByZero; finite overflow is Overflow.
pow Real exponent; a negative base requires an exactly integral exponent; other such inputs are Domain.
trig.sin, trig.cos, trig.tan Radians, every finite binary64 argument supported; infinity is Domain. Tangent overflow is Overflow.
trig.asin, trig.acos Outside [-1, 1] is Domain; angles in [-pi/2, pi/2] and [0, pi].
trig.atan, trig.atan2 Angles in [-pi/2, pi/2] and [-pi, pi]; signed zeros and infinite quadrants handled explicitly.

NaN arguments generally produce a NaN inside ok for checked functions, or a NaN value for unchecked ones. Power defines NaN^0 = 1 and 1^NaN = 1, and hypot(infinity, NaN) = infinity. exp(-infinity) = 0, expm1(-infinity) = -1, and exp2(-infinity) = 0. powi(-0, positive odd exponent) returns negative zero. Zero to the zeroth power is one. For a finite integral exponent beyond binary64’s exact consecutive integer range, parity follows the represented exponent, which is even. atan2(+0, -0) = +pi and atan2(-0, -0) = -pi; positive x preserves y’s zero sign. Sine, tangent, inverse sine, arctangent, log1p, and expm1 preserve signed zero.

Algorithms and accuracy

Roots use Newton iteration after exact exponent normalization. Logarithms use range reduction into [sqrt(1/2), sqrt(2)] and a 12-term atanh series. Exponentials use split-ln(2) range reduction and a 15-term Taylor expansion. log1p corrects the rounding error in forming 1+x; its small-input path and expm1 avoid cancellation. Integer floating powers use repeated squaring; real powers compose logarithm and exponential.

Trigonometry uses fixed-point Payne–Hanek reduction, multiplying the exact significand by 1,584 bits of 2/pi. It retains 128 fractional bits and the quadrant before applying Taylor kernels on [-pi/4, pi/4]. This supports very large arguments, including the largest finite binary64 value, without reducing against a rounded 2*pi. The table and its temporary arithmetic storage are only included when importing std/math/trig; they use 66 and 69 u64 slots, respectively. The kernels have fixed iteration counts. Inverse tangent uses reciprocal and pi/4 reductions followed by a 24-term alternating series.

The split ln(2) constants, reciprocal, exponential cutoffs, and all 66 digits of 2/pi are independently generated by scripts/generate_math_constants.py using integer bounds on elementary series at 2,048 and 4,096 bits. Both bounds must produce the same digits and binary64 rounding, and both precision settings must agree. Run python3 scripts/generate_math_constants.py --check to verify them or --write to regenerate them; only Python’s standard library is needed. The derivations and replacement provenance are recorded in stdlib/std/math/CONSTANTS.md. These independently implemented kernels have no uniform one-ulp accuracy guarantee. The table reduction follows the established Payne–Hanek method described by Hanek and Payne’s argument-reduction paper.

The regression suite compares 8,767 deterministic samples against MPFR at 256-bit precision, including exponent-stratified inputs, normal/subnormal boundaries, large angles, neighbors of multiples of pi/2, and inverse-function endpoints. Its current acceptance bounds are:

Function Tested error bound
sqrt 2e-15 * abs(reference)
cbrt, log, expm1 3e-15 * abs(reference)
log2, log10, log1p 4e-15 * abs(reference)
exp 3e-15 * abs(reference) + 2 * smallest_subnormal
exp2 4e-15 * abs(reference) + 2 * smallest_subnormal
pow 3e-13 * abs(reference) + 2 * smallest_subnormal
hypot 3e-15 * abs(reference) + 2 * smallest_subnormal
sine, cosine, arctangent/atan2, inverse sine/cosine 3e-15 absolute
tangent 2e-14 * abs(reference) + 2 * smallest_subnormal

These are reproducible regression expectations, not an exhaustive proof or a correct-rounding guarantee for every input. Relative error is unsuitable near trigonometric zeros, and tangent is ill-conditioned near its poles. Composed pow, hypot, and base-converted functions can accumulate rounding error; no uniform ulp guarantee is made for them. Applications needing certified rounding, interval enclosures, decimal arithmetic, or arbitrary precision need a separate numerical package.

python3 tests/math_reference.py regenerates the checked-in vectors using the host MPFR shared library. Normal tests need neither MPFR nor Python. The math_library test runs both fixtures at -O0 and -O3, emits WebAssembly and Cortex-M0 objects, and checks that optimized IR has no external elementary-libm calls. Windows validation runs the same fixtures through the standard-library Windows script. Object emission verifies portability of code generation, not execution on a particular board or WebAssembly runtime.

Type to search all documentation.

Keyboard shortcuts

Search documentation
Ctrl K or /
Move through results
↑ ↓
Open selected result
Enter
Close a dialog
Esc
Show these shortcuts
?