math
import math from "math";
let r: f64 = math.hypot_f64(3.0, 4.0); // 5.0
let deg: f64 = math.atan2_f64(1.0, 1.0) * 180.0 / math.PI; // 45.0
let n: i64 = math.round_f64(2.5) as i64; // 3math is pure and freestanding: it imports nothing, allocates nothing and
keeps no state, so it works in every profile, t0 kernel code included. It
is written in tape; nothing is delegated to a C library.
Naming
A function that exists for several types carries the type as a suffix:
sqrt_f64 and sqrt_f32, abs_i64 and abs_i32, min_u64. There is no
unsuffixed spelling. A few older integer helpers that exist only for i64
(gcd, lcm, div_ceil, align_up, is_power_of_two) have no suffix.
Floating-point behaviour
Every float function follows IEEE 754 and C’s <math.h> for special values:
- NaN in, NaN out, unless a function says otherwise (
pow_f64(x, 0.0)is 1 even for a NaNx;min_f64andmax_f64ignore a NaN operand). - Outside the domain (
sqrt_f64(-1.0),log_f64(-1.0),asin_f64(2.0)) the result is NaN. A domain error never traps, and there is no error value to check: test withis_nan_f64. - Overflow gives ±infinity, underflow ±0 or a subnormal.
- Signed zeros are kept:
sin_f64(-0.0)is-0.0,sqrt_f64(-0.0)is-0.0. - A NaN’s sign and payload are unspecified. Compare with
is_nan_f64, never by bits.
Accuracy
Results are within the stated error of the exact mathematical value, measured in units in the last place (ULP) of the result. “Exact” means the result is the exact value; 0.5 ULP means correctly rounded.
| Functions | f64 | f32 |
|---|---|---|
abs copysign sign min max clamp floor ceil trunc round fract fmod | exact | exact |
sqrt | 0.5 (correctly rounded) | 0.5 (correctly rounded) |
cbrt | < 0.667 | < 1 |
hypot sin cos tan asin acos atan exp exp2 log log2 log10 pow | < 1 | < 1 |
atan2 | < 2 | < 1 |
lerp | not bounded (see lerp_f64) | same |
The f32 functions compute in f64 and round once, so they are almost always
correctly rounded. sin, cos and tan reduce their argument exactly, so
they stay accurate for arguments of any size (sin_f64(1e22) is right to the
last bit).
Every function gives the same bits under tape run and in a native build,
and on every platform (apart from the sign and payload of a NaN): the code
uses only IEEE arithmetic and integer operations.
Constants
| Constant | Value |
|---|---|
PI, PI_F32 | π, 3.141592653589793 |
TAU, TAU_F32 | 2π, 6.283185307179586 |
E, E_F32 | e, 2.718281828459045 |
INF_F64, INF_F32 | +infinity (-math.INF_F64 is −infinity) |
NAN_F64, NAN_F32 | a quiet NaN |
let full_turn: f64 = math.TAU;
let never: bool = math.NAN_F64 == math.NAN_F64; // false: NaN equals nothingClassification and sign
| Function | Example |
|---|---|
is_nan_f64(x: f64) -> bool, is_nan_f32 | math.is_nan_f64(math.NAN_F64) → true |
is_inf_f64(x: f64) -> bool, is_inf_f32 | math.is_inf_f64(-math.INF_F64) → true |
is_finite_f64(x: f64) -> bool, is_finite_f32 | math.is_finite_f32(math.INF_F32) → false |
abs_f64(x: f64) -> f64, abs_f32 | math.abs_f64(-2.5) → 2.5; math.abs_f64(-0.0) → 0.0 |
copysign_f64(x: f64, y: f64) -> f64, copysign_f32 | math.copysign_f64(3.0, -0.0) → -3.0 |
sign_f64(x: f64) -> f64, sign_f32 | math.sign_f64(-7.5) → -1.0; math.sign_f64(-0.0) → -0.0 |
is_finite is false for infinities and NaN. abs clears the sign bit and
copysign takes x’s magnitude with y’s sign bit, both on NaN too. sign
is 1.0 for positive x, −1.0 for negative x, and returns zeros and NaN
unchanged.
min, max, clamp, lerp
| Function | Example |
|---|---|
min_f64(a: f64, b: f64) -> f64, min_f32 | math.min_f64(1.0, math.NAN_F64) → 1.0 |
max_f64(a: f64, b: f64) -> f64, max_f32 | math.max_f64(-0.0, 0.0) → 0.0 |
clamp_f64(x: f64, lo: f64, hi: f64) -> f64, clamp_f32 | math.clamp_f64(1.5, 0.0, 1.0) → 1.0 |
lerp_f64(a: f64, b: f64, t: f64) -> f64, lerp_f32 | math.lerp_f64(2.0, 10.0, 0.25) → 4.0 |
min and max return the other operand when one is NaN (NaN only when both
are), and order −0.0 below +0.0. clamp returns lo when x < lo, hi
when x > hi, and x otherwise, so a NaN x stays NaN. lo greater than
hi is a bug in the caller: the program stops with a trap (every clamp_*,
integer ones too). lerp is a + t * (b - a), exactly a at t = 0 and
exactly b at t = 1; t outside 0..1 extrapolates.
Rounding and remainders
| Function | Example |
|---|---|
floor_f64(x: f64) -> f64, floor_f32 | math.floor_f64(-2.5) → -3.0 |
ceil_f64(x: f64) -> f64, ceil_f32 | math.ceil_f64(-2.5) → -2.0 |
trunc_f64(x: f64) -> f64, trunc_f32 | math.trunc_f64(-2.5) → -2.0 |
round_f64(x: f64) -> f64, round_f32 | math.round_f64(-2.5) → -3.0; math.round_f32(2.5) → 3.0 |
fract_f64(x: f64) -> f64, fract_f32 | math.fract_f64(-2.75) → -0.75 |
fmod_f64(x: f64, y: f64) -> f64, fmod_f32 | math.fmod_f64(-7.5, 2.0) → -1.5 |
All of these are exact. round rounds halfway cases away from zero, like C’s
round() (round_f64(0.49999999999999994) is 0.0). fract is
x - trunc(x), so it has the sign of x. fmod is x - n*y with n the
quotient truncated toward zero, so the result has the sign of x;
fmod_f64(x, 0.0) and fmod_f64(±inf, y) are NaN. Infinities and NaN pass
through floor, ceil, trunc and round unchanged; fract of an
infinity is NaN.
Roots
| Function | Example |
|---|---|
sqrt_f64(x: f64) -> f64, sqrt_f32 | math.sqrt_f64(2.0) → 1.4142135623730951; math.sqrt_f64(-1.0) → NaN |
cbrt_f64(x: f64) -> f64, cbrt_f32 | math.cbrt_f64(-27.0) → -3.0 |
hypot_f64(x: f64, y: f64) -> f64, hypot_f32 | math.hypot_f64(3.0, 4.0) → 5.0; math.hypot_f64(1e300, 1e300) → 1.4142135623730952e300 |
sqrt is correctly rounded; it is NaN below zero. cbrt is defined for
negative numbers. hypot is sqrt(x*x + y*y) without intermediate overflow
or underflow, and is +infinity when either argument is infinite, even if the
other is NaN.
Trigonometry
Angles are in radians.
| Function | Example |
|---|---|
sin_f64(x: f64) -> f64, sin_f32 | math.sin_f64(math.PI / 6.0) → 0.49999999999999994; math.sin_f32(1.0) → 0.84147096 |
cos_f64(x: f64) -> f64, cos_f32 | math.cos_f64(math.PI) → -1.0 |
tan_f64(x: f64) -> f64, tan_f32 | math.tan_f64(math.PI / 4.0) → 0.9999999999999999 |
asin_f64(x: f64) -> f64, asin_f32 | math.asin_f64(1.0) → 1.5707963267948966 |
acos_f64(x: f64) -> f64, acos_f32 | math.acos_f64(-1.0) → 3.141592653589793; math.acos_f64(2.0) → NaN |
atan_f64(x: f64) -> f64, atan_f32 | math.atan_f64(1.0) → 0.7853981633974483 |
atan2_f64(y: f64, x: f64) -> f64, atan2_f32 | math.atan2_f64(1.0, -1.0) → 2.356194490192345 |
sin_f64(math.PI / 6.0) is not exactly 0.5 because math.PI is not exactly
π. sin, cos and tan of an infinity are NaN. asin and acos are NaN
outside −1..1; asin returns −π/2..π/2, acos 0..π. atan returns
−π/2..π/2 and atan2(y, x) the angle of the point (x, y), −π..π, with C’s
results for zeros and infinities: atan2_f64(-0.0, -1.0) is −π,
atan2_f64(1.0, -math.INF_F64) is π, atan2_f64(math.INF_F64, math.INF_F64)
is π/4.
Exponentials and logarithms
| Function | Example |
|---|---|
exp_f64(x: f64) -> f64, exp_f32 | math.exp_f64(1.0) → 2.7182818284590455; math.exp_f64(710.0) → inf |
exp2_f64(x: f64) -> f64, exp2_f32 | math.exp2_f64(10.0) → 1024.0; math.exp2_f64(-1074.0) → 5e-324 |
log_f64(x: f64) -> f64, log_f32 | math.log_f64(math.E) → 1.0; math.log_f64(0.0) → −inf |
log2_f64(x: f64) -> f64, log2_f32 | math.log2_f64(1024.0) → 10.0 |
log10_f64(x: f64) -> f64, log10_f32 | math.log10_f64(1000.0) → 3.0 |
pow_f64(x: f64, y: f64) -> f64, pow_f32 | math.pow_f64(2.0, 10.0) → 1024.0; math.pow_f64(1.5, 2.5) → 2.7556759606310752 |
exp is e^x and exp2 2^x; both overflow to +infinity and underflow to 0
(exp_f64(-math.INF_F64) is 0), and exp2 of an integer is exact. exp_f64(1.0) is one ULP above E, within the bound. log is the
natural logarithm; the logarithms are −infinity at ±0, NaN below zero, and
exact (0.0) at 1. log2 of a power of two is exact.
pow follows C99 for special cases:
| Case | Result |
|---|---|
pow(x, ±0), pow(1, y) | 1, even for NaN |
pow(x, y) with x < 0 finite, y finite and not an integer | NaN (pow_f64(-8.0, 1.0 / 3.0)) |
pow(±0, y) with y < 0 | ±infinity for odd integer y (sign of the zero), else +infinity |
pow(±0, y) with y > 0 | ±0 for odd integer y, else +0 |
pow(-1, ±inf) | 1 |
pow(x, -inf) | +infinity for |x| < 1, +0 for |x| > 1 |
pow(x, +inf) | +0 for |x| < 1, +infinity for |x| > 1 |
pow(-inf, y) | −0 / −infinity for odd integer y < 0 / > 0, else +0 / +infinity |
pow(+inf, y) | +0 for y < 0, +infinity for y > 0 |
A negative base with an integer exponent works: pow_f64(-2.0, 3.0) is
-8.0.
Integers
| Function | Example |
|---|---|
abs_i64(x: i64) -> i64, abs_i32 | math.abs_i64(-7) → 7 |
min_i64(a: i64, b: i64) -> i64, min_i32 | math.min_i64(3, -4) → -4 |
max_i64(a: i64, b: i64) -> i64, max_i32 | math.max_i64(3, -4) → 3 |
clamp_i64(x: i64, lo: i64, hi: i64) -> i64, clamp_i32 | math.clamp_i64(12, 0, 10) → 10 |
sign_i64(x: i64) -> i64, sign_i32 | math.sign_i64(-9) → -1 |
pow_i64(base: i64, exp: i64) -> i64 | math.pow_i64(3, 4) → 81 |
sqrt_i64(x: i64) -> i64 | math.sqrt_i64(99) → 9 |
log2_i64(x: i64) -> i64 | math.log2_i64(1000) → 9 |
gcd(a: i64, b: i64) -> i64 | math.gcd(12, 18) → 6 |
lcm(a: i64, b: i64) -> i64 | math.lcm(4, 6) → 12 |
div_ceil(a: i64, b: i64) -> i64 | math.div_ceil(7, 2) → 4 |
align_up(x: i64, align: i64) -> i64 | math.align_up(13, 8) → 16 |
is_power_of_two(x: i64) -> bool | math.is_power_of_two(64) → true |
div_floor_i64(a: i64, b: i64) -> i64 | math.div_floor_i64(-7, 2) → -4 |
mod_floor_i64(a: i64, b: i64) -> i64 | math.mod_floor_i64(-1, 5) → 4 |
min_u64, max_u64, clamp_u64 | math.max_u64(3, 0xFFFFFFFFFFFFFFFF) → 18446744073709551615 |
pow_u64(base: u64, exp: u64) -> u64 | math.pow_u64(3, 40) → 12157665459056928801 |
sqrt_u64(x: u64) -> u64 | math.sqrt_u64(0xFFFFFFFFFFFFFFFF) → 4294967295 |
div_ceil_u64(a: u64, b: u64) -> u64 | math.div_ceil_u64(7, 2) → 4 |
is_power_of_two_u64(x: u64) -> bool | math.is_power_of_two_u64(0) → false |
next_power_of_two_u64(x: u64) -> u64 | math.next_power_of_two_u64(1025) → 2048 |
Integer arithmetic wraps on overflow, in tape generally (+, - and *
work modulo 2^64) and in these helpers (abs_i64 of the most negative value
is itself, pow_i64 and pow_u64 wrap). pow_i64 is 1 for exp <= 0. sqrt_i64 is the
floor of the square root, 0 for x <= 0; log2_i64 the floor of the
base-2 logarithm, −1 for x <= 0. gcd is the non-negative greatest
common divisor and gcd(0, 0) is 0; lcm is 0 when either argument is.
div_ceil divides rounding toward +infinity for positive b; with b = 0
it divides by zero, which stops the program. align_up needs a power-of-two
align.
div_floor_i64 rounds the quotient toward −infinity and mod_floor_i64 is
the matching remainder, with the sign of b (Python’s // and %), so
a == b * div_floor_i64(a, b) + mod_floor_i64(a, b) always holds. / and
% truncate toward zero instead. Both trap for b = 0; div_ceil_u64
too. next_power_of_two_u64 is the smallest power of two >= x (1 for 0)
and traps above 2^63, where the answer doesn’t fit.
Checked arithmetic
| Function | Example |
|---|---|
checked_add_i64(a: i64, b: i64) -> ?i64 | math.checked_add_i64(9223372036854775807, 1) → null |
checked_sub_i64(a: i64, b: i64) -> ?i64 | math.checked_sub_i64(2, 3) → -1 |
checked_mul_i64(a: i64, b: i64) -> ?i64 | math.checked_mul_i64(3037000500, 3037000500) → null |
checked_add_u64, checked_sub_u64, checked_mul_u64 | math.checked_sub_u64(4, 5) → null |
The result, or null when it doesn’t fit in the type. Use them where an
overflow would be a wrong answer (sizes from untrusted input, counters that
must not wrap):
let total: ?i64 = math.checked_mul_i64(count, size);
if (total == null) {
return 1; // too large
}
let bytes: i64 = total!;Testing
tape test stdlib/math.tape checks every function against reference values
computed to high precision, special values, symmetries, and large arguments
of the trigonometric functions, in the VM and natively.
Last modified: