math

tape
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;                // 3

math 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 NaN x; min_f64 and max_f64 ignore 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 with is_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.

Functionsf64f32
abs copysign sign min max clamp floor ceil trunc round fract fmodexactexact
sqrt0.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
lerpnot 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

ConstantValue
PI, PI_F32π, 3.141592653589793
TAU, TAU_F322π, 6.283185307179586
E, E_F32e, 2.718281828459045
INF_F64, INF_F32+infinity (-math.INF_F64 is −infinity)
NAN_F64, NAN_F32a quiet NaN
tape
let full_turn: f64 = math.TAU;
let never: bool = math.NAN_F64 == math.NAN_F64;   // false: NaN equals nothing

Classification and sign

FunctionExample
is_nan_f64(x: f64) -> bool, is_nan_f32math.is_nan_f64(math.NAN_F64) → true
is_inf_f64(x: f64) -> bool, is_inf_f32math.is_inf_f64(-math.INF_F64) → true
is_finite_f64(x: f64) -> bool, is_finite_f32math.is_finite_f32(math.INF_F32) → false
abs_f64(x: f64) -> f64, abs_f32math.abs_f64(-2.5) → 2.5; math.abs_f64(-0.0) → 0.0
copysign_f64(x: f64, y: f64) -> f64, copysign_f32math.copysign_f64(3.0, -0.0) → -3.0
sign_f64(x: f64) -> f64, sign_f32math.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

FunctionExample
min_f64(a: f64, b: f64) -> f64, min_f32math.min_f64(1.0, math.NAN_F64) → 1.0
max_f64(a: f64, b: f64) -> f64, max_f32math.max_f64(-0.0, 0.0) → 0.0
clamp_f64(x: f64, lo: f64, hi: f64) -> f64, clamp_f32math.clamp_f64(1.5, 0.0, 1.0) → 1.0
lerp_f64(a: f64, b: f64, t: f64) -> f64, lerp_f32math.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

FunctionExample
floor_f64(x: f64) -> f64, floor_f32math.floor_f64(-2.5) → -3.0
ceil_f64(x: f64) -> f64, ceil_f32math.ceil_f64(-2.5) → -2.0
trunc_f64(x: f64) -> f64, trunc_f32math.trunc_f64(-2.5) → -2.0
round_f64(x: f64) -> f64, round_f32math.round_f64(-2.5) → -3.0; math.round_f32(2.5) → 3.0
fract_f64(x: f64) -> f64, fract_f32math.fract_f64(-2.75) → -0.75
fmod_f64(x: f64, y: f64) -> f64, fmod_f32math.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

FunctionExample
sqrt_f64(x: f64) -> f64, sqrt_f32math.sqrt_f64(2.0) → 1.4142135623730951; math.sqrt_f64(-1.0) → NaN
cbrt_f64(x: f64) -> f64, cbrt_f32math.cbrt_f64(-27.0) → -3.0
hypot_f64(x: f64, y: f64) -> f64, hypot_f32math.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.

FunctionExample
sin_f64(x: f64) -> f64, sin_f32math.sin_f64(math.PI / 6.0) → 0.49999999999999994; math.sin_f32(1.0) → 0.84147096
cos_f64(x: f64) -> f64, cos_f32math.cos_f64(math.PI) → -1.0
tan_f64(x: f64) -> f64, tan_f32math.tan_f64(math.PI / 4.0) → 0.9999999999999999
asin_f64(x: f64) -> f64, asin_f32math.asin_f64(1.0) → 1.5707963267948966
acos_f64(x: f64) -> f64, acos_f32math.acos_f64(-1.0) → 3.141592653589793; math.acos_f64(2.0) → NaN
atan_f64(x: f64) -> f64, atan_f32math.atan_f64(1.0) → 0.7853981633974483
atan2_f64(y: f64, x: f64) -> f64, atan2_f32math.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

FunctionExample
exp_f64(x: f64) -> f64, exp_f32math.exp_f64(1.0) → 2.7182818284590455; math.exp_f64(710.0) → inf
exp2_f64(x: f64) -> f64, exp2_f32math.exp2_f64(10.0) → 1024.0; math.exp2_f64(-1074.0) → 5e-324
log_f64(x: f64) -> f64, log_f32math.log_f64(math.E) → 1.0; math.log_f64(0.0) → −inf
log2_f64(x: f64) -> f64, log2_f32math.log2_f64(1024.0) → 10.0
log10_f64(x: f64) -> f64, log10_f32math.log10_f64(1000.0) → 3.0
pow_f64(x: f64, y: f64) -> f64, pow_f32math.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:

CaseResult
pow(x, ±0), pow(1, y)1, even for NaN
pow(x, y) with x < 0 finite, y finite and not an integerNaN (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

FunctionExample
abs_i64(x: i64) -> i64, abs_i32math.abs_i64(-7) → 7
min_i64(a: i64, b: i64) -> i64, min_i32math.min_i64(3, -4) → -4
max_i64(a: i64, b: i64) -> i64, max_i32math.max_i64(3, -4) → 3
clamp_i64(x: i64, lo: i64, hi: i64) -> i64, clamp_i32math.clamp_i64(12, 0, 10) → 10
sign_i64(x: i64) -> i64, sign_i32math.sign_i64(-9) → -1
pow_i64(base: i64, exp: i64) -> i64math.pow_i64(3, 4) → 81
sqrt_i64(x: i64) -> i64math.sqrt_i64(99) → 9
log2_i64(x: i64) -> i64math.log2_i64(1000) → 9
gcd(a: i64, b: i64) -> i64math.gcd(12, 18) → 6
lcm(a: i64, b: i64) -> i64math.lcm(4, 6) → 12
div_ceil(a: i64, b: i64) -> i64math.div_ceil(7, 2) → 4
align_up(x: i64, align: i64) -> i64math.align_up(13, 8) → 16
is_power_of_two(x: i64) -> boolmath.is_power_of_two(64) → true
div_floor_i64(a: i64, b: i64) -> i64math.div_floor_i64(-7, 2) → -4
mod_floor_i64(a: i64, b: i64) -> i64math.mod_floor_i64(-1, 5) → 4
min_u64, max_u64, clamp_u64math.max_u64(3, 0xFFFFFFFFFFFFFFFF) → 18446744073709551615
pow_u64(base: u64, exp: u64) -> u64math.pow_u64(3, 40) → 12157665459056928801
sqrt_u64(x: u64) -> u64math.sqrt_u64(0xFFFFFFFFFFFFFFFF) → 4294967295
div_ceil_u64(a: u64, b: u64) -> u64math.div_ceil_u64(7, 2) → 4
is_power_of_two_u64(x: u64) -> boolmath.is_power_of_two_u64(0) → false
next_power_of_two_u64(x: u64) -> u64math.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

FunctionExample
checked_add_i64(a: i64, b: i64) -> ?i64math.checked_add_i64(9223372036854775807, 1) → null
checked_sub_i64(a: i64, b: i64) -> ?i64math.checked_sub_i64(2, 3) → -1
checked_mul_i64(a: i64, b: i64) -> ?i64math.checked_mul_i64(3037000500, 3037000500) → null
checked_add_u64, checked_sub_u64, checked_mul_u64math.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):

tape
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: