(rontolisp) docs

Math Function Backends

Arithmetic and comparison operators work on both integers and doubles. When any operand is a double, the result is promoted to double (e.g., (+ 1 1.5) returns 2.5). +, -, *, / accept two or more arguments. mod supports doubles in the interpreter and JVM compiler but not in the WASM compiler.

Math function backend support

Every transcendental below is ONE algorithm on every backend -- fdlibm, the one java.lang.StrictMath specifies -- so (exp x), (sin x) and the rest print the same digits on the interpreter, on the JVM whatever the CPU, and on both WASM targets, with or without --simd. The interpreter and the JVM call StrictMath; WASM has no transcendental instruction, so the compiled module carries the same fdlibm as a set of runtime functions. Only --gpu's element-wise transcendentals are the device's own (GPU acceleration). What differs between the backends is how widely the other math built-ins are supported:

  • sqrt, isqrt, gcd, lcm, signum, expt are supported on all three backends (interpreter, JVM, WASM) and through the compiled eval. sqrt uses the native f64.sqrt instruction.
  • exp is supported on all three backends, fdlibm's on each: (exp 1.0) is 2.7182818284590455 everywhere. The IEEE edges are fdlibm's ((exp 709.78) is 1.7928227943945155e308, (exp 710.0) is Infinity, (exp -1000.0) and (exp -1e30) are 0.0, a NaN argument is NaN), which is what lets a -infinity mask reach a masked softmax as a weight of exactly 0.0 -- so a sigmoid (/ 1.0 (+ 1.0 (exp (- 0 x)))) or a softmax over whole-vector kernels compiled to WASM prints what the JVM prints.
  • log and tanh likewise, fdlibm's on every backend. (log 1) and (tanh 0) are exactly 0.0; (log 0.0) is -Infinity, while a NEGATIVE argument crosses into the complex plane everywhere -- (log -1) is #C(0.0 pi) -- instead of answering NaN; tanh is odd, so (tanh -0.0) is -0.0, and it saturates to exactly ±1.0 past |x| = 22.
  • sin, cos and tan likewise, fdlibm's on every backend, including its exact argument reduction for large arguments: (sin 1e22) is -0.8522008497671888 everywhere. (sin 0) is 0.0, (cos 0) is 1.0, (sin (/ pi 2)) is 1.0, (cos pi) is -1.0; NaN/±Infinity arguments give NaN; sin and tan are odd, so (sin -0.0) and (tan -0.0) are -0.0.
  • asin, acos and atan likewise, fdlibm's on every backend. (atan 0), (asin 0) and (acos 1) are exactly 0.0, (asin ±1) is exactly ±pi/2 and (acos -1) exactly pi; an asin/acos argument outside [-1, 1] crosses into the complex plane everywhere, the way sqrt roots a negative, rather than answering NaN.
  • sinh and cosh likewise, fdlibm's on every backend (computed over its expm1, so a tiny argument does not cancel). (sinh 0) is exactly 0.0 and (cosh 0) exactly 1.0; (sinh ±Infinity) is ±Infinity and (cosh ±Infinity) is Infinity, and both overflow to Infinity at the same edge everywhere. With these, every transcendental built-in works on all three backends, with one set of digits.
  • expt keeps an exact rational result for an integer or ratio base raised to an integer exponent (with big-integer promotion on every backend -- the WASM expt, like all WASM integer arithmetic, stays exact at any magnitude); a negative exponent yields the reciprocal ((expt 2 -1) is 1/2). A float anywhere, or a ratio exponent, makes the answer fdlibm's pow on every backend (StrictMath.pow on the interpreter and the JVM; (expt 2.0 3) is 8.0, (expt 10000.0 0.75) is 1000.0, (expt 1.1 10) is 2.5937424601000023 everywhere), with pow's edges (x^0.0 is 1.0, 0^y is 0.0 for a positive and Infinity for a negative y). A negative base to a non-integer power crosses into the complex plane on every backend rather than answering NaN: the modulus |base|^power turned through power * pi radians, an exactly known phase that the complex exp(w * log z) form would have to recover from a logarithm. An integer-valued float exponent ((expt 2 3.0)) is 8.0 exactly, pow's own answer. The dispatch happens at run time on every backend, so an exponent that arrives through a variable or a function call behaves exactly like a literal one.
  • asinh, acosh, atanh and cis are supported on all three backends. java.lang.Math has no inverse hyperbolic, so all three are one hand-rolled formula each over fdlibm's log1p, log and hypot, evaluated the same way by the interpreter, the JVM and the WASM backends, so the bits agree everywhere. A real argument outside the real domain crosses into the complex plane the way sqrt roots a negative -- (acosh 0) and (atanh 2) answer complex -- so the JVM and WASM call sites carry the plane as a runtime branch like sqrt does. cis always answers a complex: (cis x) is (cos x, sin x), and a complex argument decays by e^{-im}. (asinh 0), (acosh 1) and (atanh 0) are exactly 0.0 and (cis 0) exactly #C(1.0 0.0) on every backend.
  • gcd, lcm, signum are exact at any magnitude on every backend; isqrt still operates on the i31 integer range in the WASM backend.
  • floor, ceiling, round, truncate are exact at any magnitude on every backend, in both of the values they return. A finite float is already a mathematical integer above 2^52, and a division of two floats has exactly one mathematical quotient, so the quotient is that integer -- promoted to a big integer when it exceeds the fixnum range, like every other numeric operator, never clamped to it. (floor 1d300) is the exact 301-digit value of that double, and (truncate 1d300 7.0) its exact quotient with a remainder of 1.0. The second value satisfies quotient*divisor + remainder = number and is the same quantity rem answers for truncate and mod for floor. This is not only about huge values: the double 0.1 is a shade above a tenth, so (floor 1.0 0.1) is 9 with a remainder of 0.09999999999999995 -- the quotient of the two values as they actually are, and the same remainder mod gives -- where rounding 1.0/0.1 to 10.0 first would answer 10. The --no-gc WASM lowering is the exception to the exactness: its integers are 64-bit by design, so a quotient past that range has no representation there.
  • random is supported on all three backends. It returns a non-negative random number below the (positive) limit, of the same type as the limit (an integer limit yields an integer, a float limit a float). The integer and float paths are chosen from the literal shape of the argument, so use a float literal ((random 1.0)) when a float result is wanted. Every backend draws from a generator inside the program — java.util.concurrent.ThreadLocalRandom on the interpreter and JVM, a built-in generator on WASM — which is seeded once per run from the host's entropy where there is a host (the real wasi_snapshot_preview1.random_get in Preview 1 mode, wasi:random@0.3.0 in --component mode), so the sequence differs each run. random is not available inside the compiled eval.
  • logand, logior, logxor, lognot, ash are supported on all three backends and compute on exact integers of any magnitude (ash shifts left for a non-negative count, right otherwise).