From fd7dad65796e1edcedea24dc9768f5dc588433c9 Mon Sep 17 00:00:00 2001 From: Bradley Lewis Fargo Date: Thu, 19 Mar 2026 01:48:59 -0500 Subject: [PATCH] Fix IEEE 754 compliance for real number operations Erlang's :math module raises ArithmeticError for overflow and domain errors instead of returning IEEE 754 special values. This causes downstream libraries (like Nx) to crash on valid inputs. Fixes: - exp(large): returns :infinity instead of crashing - sinh(large): returns :infinity/:neg_infinity based on sign - cosh(large): returns :infinity - asin/acos outside [-1,1]: returns :nan instead of crashing - acosh below 1: returns :nan - atanh outside (-1,1): returns :nan - atanh(1): returns :infinity, atanh(-1): returns :neg_infinity - sqrt(negative): returns :nan instead of crashing - pow(0, negative): returns :infinity instead of crashing - divide(x, 0.0): returns :infinity/:neg_infinity/:nan per IEEE 754 - divide(x, -0.0): respects negative zero sign (OTP 27+ matching) - cot(0): returns :infinity instead of crashing (1/tan(0) = 1/0) - acot(0): returns pi/2 instead of crashing - acsc(0): returns :infinity instead of crashing - acsc(values < 1): returns :nan for domain errors 34 new tests covering all IEEE 754 edge cases. All 169 tests pass (99 doctests + 70 tests). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/complex.ex | 100 ++++++++++++++++++++--- test/complex_test.exs | 184 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 12 deletions(-) diff --git a/lib/complex.ex b/lib/complex.ex index 790b2ec..6d8a234 100644 --- a/lib/complex.ex +++ b/lib/complex.ex @@ -576,6 +576,15 @@ defmodule Complex do def divide(a, :infinity) when is_number(a), do: 0 def divide(a, :neg_infinity) when is_number(a), do: 0 + def divide(x, +0.0) when is_number(x) and x > 0, do: :infinity + def divide(x, +0.0) when is_number(x) and x < 0, do: :neg_infinity + def divide(x, +0.0) when is_number(x), do: :nan + def divide(x, -0.0) when is_number(x) and x > 0, do: :neg_infinity + def divide(x, -0.0) when is_number(x) and x < 0, do: :infinity + def divide(x, -0.0) when is_number(x), do: :nan + def divide(x, 0) when is_number(x) and x > 0, do: :infinity + def divide(x, 0) when is_number(x) and x < 0, do: :neg_infinity + def divide(x, 0) when is_number(x), do: :nan def divide(x, y) when is_number(x) and is_number(y), do: x / y def divide(n, b) when is_number(n) and b in [:infinity, :neg_infinity] do @@ -779,7 +788,12 @@ defmodule Complex do def sqrt(:infinity), do: :infinity def sqrt(:neg_infinity), do: :nan def sqrt(:nan), do: :nan - def sqrt(n) when is_number(n), do: :math.sqrt(n) + + def sqrt(n) when is_number(n) do + :math.sqrt(n) + rescue + ArithmeticError -> :nan + end def sqrt(%Complex{re: :nan}), do: Complex.new(:nan, :nan) def sqrt(%Complex{im: :nan}), do: Complex.new(:nan, :nan) @@ -882,7 +896,12 @@ defmodule Complex do def exp(:infinity), do: :infinity def exp(:neg_infinity), do: 0 def exp(:nan), do: :nan - def exp(n) when is_number(n), do: :math.exp(n) + + def exp(n) when is_number(n) do + :math.exp(n) + rescue + ArithmeticError -> :infinity + end def exp(%Complex{re: :neg_infinity, im: _}), do: new(0, 0) def exp(%Complex{re: :infinity, im: :nan}), do: new(:infinity, :nan) @@ -1126,7 +1145,16 @@ defmodule Complex do def pow(_, :infinity), do: :infinity def pow(x, y) when is_integer(x) and is_integer(y) and y >= 0, do: Integer.pow(x, y) - def pow(x, y) when is_number(x) and is_number(y), do: :math.pow(x, y) + + def pow(x, y) when is_number(x) and is_number(y) do + :math.pow(x, y) + rescue + ArithmeticError -> + cond do + x == 0 and y < 0 -> :infinity + true -> :nan + end + end def pow(x, y) do x = as_complex(x) @@ -1243,7 +1271,11 @@ defmodule Complex do @spec asin(t | number | non_finite_number) :: t | number | non_finite_number def asin(z) - def asin(n) when is_number(n), do: :math.asin(n) + def asin(n) when is_number(n) do + :math.asin(n) + rescue + ArithmeticError -> :nan + end def asin(n) when is_non_finite_number(n), do: :nan @@ -1317,7 +1349,11 @@ defmodule Complex do @spec acos(t | number | non_finite_number) :: t | number | non_finite_number def acos(z) - def acos(n) when is_number(n), do: :math.acos(n) + def acos(n) when is_number(n) do + :math.acos(n) + rescue + ArithmeticError -> :nan + end def acos(n) when is_non_finite_number(n), do: :nan @@ -1452,7 +1488,11 @@ defmodule Complex do @spec cot(t | number | non_finite_number) :: t | number | non_finite_number def cot(z) - def cot(n) when is_number(n), do: 1 / :math.tan(n) + def cot(n) when is_number(n) do + 1 / :math.tan(n) + rescue + ArithmeticError -> :infinity + end def cot(z) do divide(cos(z), sin(z)) @@ -1478,7 +1518,11 @@ defmodule Complex do @spec acot(t | number | non_finite_number) :: t | number | non_finite_number def acot(z) - def acot(n) when is_number(n), do: :math.atan(1 / n) + def acot(n) when is_number(n) do + :math.atan(1 / n) + rescue + ArithmeticError -> :math.pi() / 2 + end def acot(:infinity), do: 0 def acot(:neg_infinity), do: :math.pi() @@ -1592,7 +1636,13 @@ defmodule Complex do @spec acsc(t | number | non_finite_number) :: t | number | non_finite_number def acsc(z) - def acsc(n) when is_number(n), do: :math.asin(1 / n) + def acsc(n) when is_number(n) do + :math.asin(1 / n) + rescue + ArithmeticError -> + if n == 0, do: :infinity, else: :nan + end + def acsc(:infinity), do: 0 def acsc(:neg_infinity), do: -:math.pi() def acsc(:nan), do: :nan @@ -1630,7 +1680,11 @@ defmodule Complex do def sinh(n) when is_non_finite_number(n), do: n - def sinh(n) when is_number(n), do: :math.sinh(n) + def sinh(n) when is_number(n) do + :math.sinh(n) + rescue + ArithmeticError -> if n > 0, do: :infinity, else: :neg_infinity + end def sinh(z = %Complex{}) do %Complex{re: re, im: im} = @@ -1703,7 +1757,12 @@ defmodule Complex do def cosh(:infinity), do: :infinity def cosh(:neg_infinity), do: :infinity def cosh(:nan), do: :nan - def cosh(n) when is_number(n), do: :math.cosh(n) + + def cosh(n) when is_number(n) do + :math.cosh(n) + rescue + ArithmeticError -> :infinity + end def cosh(z) do %Complex{re: re, im: im} = @@ -1732,10 +1791,16 @@ defmodule Complex do def acosh(z) if math_fun_supported?.(:acosh, 1) do - def acosh(n) when is_number(n), do: :math.acosh(n) + def acosh(n) when is_number(n) do + :math.acosh(n) + rescue + ArithmeticError -> :nan + end else def acosh(n) when is_number(n) do :math.log(n + :math.sqrt(n * n - 1)) + rescue + ArithmeticError -> :nan end end @@ -1798,11 +1863,22 @@ defmodule Complex do @spec atanh(t | number | non_finite_number) :: t | number | non_finite_number def atanh(z) + def atanh(1), do: :infinity + def atanh(1.0), do: :infinity + def atanh(-1), do: :neg_infinity + def atanh(-1.0), do: :neg_infinity + if math_fun_supported?.(:atanh, 1) do - def atanh(n) when is_number(n), do: :math.atanh(n) + def atanh(n) when is_number(n) do + :math.atanh(n) + rescue + ArithmeticError -> :nan + end else def atanh(n) when is_number(n) do 0.5 * :math.log((1 + n) / (1 - n)) + rescue + ArithmeticError -> :nan end end diff --git a/test/complex_test.exs b/test/complex_test.exs index 1925725..b186b6f 100644 --- a/test/complex_test.exs +++ b/test/complex_test.exs @@ -1017,4 +1017,188 @@ defmodule ComplexTest do """) end end + + # ── IEEE 754 compliance ────────────────────────────────────────── + + describe "IEEE 754: overflow returns Inf instead of crashing" do + test "exp(large) returns :infinity" do + assert Complex.exp(1000) == :infinity + assert Complex.exp(1000.0) == :infinity + end + + test "sinh(large positive) returns :infinity" do + assert Complex.sinh(1000) == :infinity + assert Complex.sinh(1000.0) == :infinity + end + + test "sinh(large negative) returns :neg_infinity" do + assert Complex.sinh(-1000) == :neg_infinity + assert Complex.sinh(-1000.0) == :neg_infinity + end + + test "cosh(large) returns :infinity" do + assert Complex.cosh(1000) == :infinity + assert Complex.cosh(1000.0) == :infinity + end + end + + describe "IEEE 754: domain errors return NaN instead of crashing" do + test "asin outside [-1, 1]" do + assert Complex.asin(2.0) == :nan + assert Complex.asin(-2.0) == :nan + end + + test "acos outside [-1, 1]" do + assert Complex.acos(2.0) == :nan + assert Complex.acos(-2.0) == :nan + end + + test "acosh below 1" do + assert Complex.acosh(0.5) == :nan + assert Complex.acosh(-1.0) == :nan + end + + test "atanh outside (-1, 1)" do + assert Complex.atanh(2.0) == :nan + assert Complex.atanh(-2.0) == :nan + end + + test "atanh at boundaries" do + assert Complex.atanh(1.0) == :infinity + assert Complex.atanh(1) == :infinity + assert Complex.atanh(-1.0) == :neg_infinity + assert Complex.atanh(-1) == :neg_infinity + end + end + + describe "IEEE 754: division by zero" do + test "positive / 0.0 = :infinity" do + assert Complex.divide(1.0, 0.0) == :infinity + assert Complex.divide(5, 0.0) == :infinity + end + + test "negative / 0.0 = :neg_infinity" do + assert Complex.divide(-1.0, 0.0) == :neg_infinity + assert Complex.divide(-5, 0.0) == :neg_infinity + end + + test "0.0 / 0.0 = :nan" do + assert Complex.divide(0.0, 0.0) == :nan + assert Complex.divide(0, 0.0) == :nan + end + + test "positive / -0.0 = :neg_infinity" do + assert Complex.divide(1.0, -0.0) == :neg_infinity + end + + test "negative / -0.0 = :infinity" do + assert Complex.divide(-1.0, -0.0) == :infinity + end + + test "normal division still works" do + assert Complex.divide(6.0, 3.0) == 2.0 + assert Complex.divide(10, 2) == 5 + end + end + + describe "IEEE 754: sqrt domain errors" do + test "sqrt(negative) returns :nan" do + assert Complex.sqrt(-1) == :nan + assert Complex.sqrt(-1.0) == :nan + assert Complex.sqrt(-4.0) == :nan + end + end + + describe "IEEE 754: pow edge cases" do + test "pow(0, negative) returns :infinity" do + assert Complex.pow(0, -1) == :infinity + assert Complex.pow(0.0, -1.0) == :infinity + end + end + + describe "IEEE 754: cot/acot/acsc domain errors" do + test "cot(0) returns :infinity (1/tan(0) = 1/0)" do + assert Complex.cot(0) == :infinity + assert Complex.cot(0.0) == :infinity + end + + test "acot(0) returns pi/2" do + assert_close Complex.acot(0), :math.pi() / 2 + assert_close Complex.acot(0.0), :math.pi() / 2 + end + + test "acsc(0) returns :infinity" do + assert Complex.acsc(0) == :infinity + assert Complex.acsc(0.0) == :infinity + end + + test "acsc(0.5) returns :nan (asin(2) domain error)" do + assert Complex.acsc(0.5) == :nan + end + end + + describe "IEEE 754: integer division by zero" do + test "divide(1, 0) returns :infinity" do + assert Complex.divide(1, 0) == :infinity + end + + test "divide(-1, 0) returns :neg_infinity" do + assert Complex.divide(-1, 0) == :neg_infinity + end + + test "divide(0, 0) returns :nan" do + assert Complex.divide(0, 0) == :nan + end + end + + describe "IEEE 754: pow domain errors" do + test "pow(-1, 0.5) returns :nan (sqrt of negative)" do + assert Complex.pow(-1, 0.5) == :nan + assert Complex.pow(-1.0, 0.5) == :nan + end + end + + describe "IEEE 754: log edge cases" do + test "log(0) returns :neg_infinity" do + assert Complex.log(0) == :neg_infinity + assert Complex.log(0.0) == :neg_infinity + end + + test "log(negative) returns :nan" do + assert Complex.log(-1.0) == :nan + end + + test "log10(0) returns :neg_infinity" do + assert Complex.log10(0) == :neg_infinity + end + + test "log2(0) returns :neg_infinity" do + assert Complex.log2(0) == :neg_infinity + end + end + + describe "IEEE 754: tanh at extremes" do + test "tanh(large) clamps to 1/-1" do + assert Complex.tanh(1000) == 1.0 + assert Complex.tanh(-1000) == -1.0 + end + end + + describe "IEEE 754: normal values still work" do + test "exp(0) == 1" do + assert Complex.exp(0) == 1.0 + end + + test "asin(0.5) is correct" do + assert_close Complex.asin(0.5), :math.asin(0.5) + end + + test "sinh(1) is correct" do + assert_close Complex.sinh(1.0), :math.sinh(1.0) + end + + test "cosh(1) is correct" do + assert_close Complex.cosh(1.0), :math.cosh(1.0) + end + end end