Skip to content

Commit ab2149e

Browse files
Fix IEEE 754 compliance for real number operations (#29)
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) <noreply@anthropic.com>
1 parent 17344af commit ab2149e

File tree

2 files changed

+272
-12
lines changed

2 files changed

+272
-12
lines changed

lib/complex.ex

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,15 @@ defmodule Complex do
576576
def divide(a, :infinity) when is_number(a), do: 0
577577
def divide(a, :neg_infinity) when is_number(a), do: 0
578578

579+
def divide(x, +0.0) when is_number(x) and x > 0, do: :infinity
580+
def divide(x, +0.0) when is_number(x) and x < 0, do: :neg_infinity
581+
def divide(x, +0.0) when is_number(x), do: :nan
582+
def divide(x, -0.0) when is_number(x) and x > 0, do: :neg_infinity
583+
def divide(x, -0.0) when is_number(x) and x < 0, do: :infinity
584+
def divide(x, -0.0) when is_number(x), do: :nan
585+
def divide(x, 0) when is_number(x) and x > 0, do: :infinity
586+
def divide(x, 0) when is_number(x) and x < 0, do: :neg_infinity
587+
def divide(x, 0) when is_number(x), do: :nan
579588
def divide(x, y) when is_number(x) and is_number(y), do: x / y
580589

581590
def divide(n, b) when is_number(n) and b in [:infinity, :neg_infinity] do
@@ -779,7 +788,12 @@ defmodule Complex do
779788
def sqrt(:infinity), do: :infinity
780789
def sqrt(:neg_infinity), do: :nan
781790
def sqrt(:nan), do: :nan
782-
def sqrt(n) when is_number(n), do: :math.sqrt(n)
791+
792+
def sqrt(n) when is_number(n) do
793+
:math.sqrt(n)
794+
rescue
795+
ArithmeticError -> :nan
796+
end
783797

784798
def sqrt(%Complex{re: :nan}), do: Complex.new(:nan, :nan)
785799
def sqrt(%Complex{im: :nan}), do: Complex.new(:nan, :nan)
@@ -882,7 +896,12 @@ defmodule Complex do
882896
def exp(:infinity), do: :infinity
883897
def exp(:neg_infinity), do: 0
884898
def exp(:nan), do: :nan
885-
def exp(n) when is_number(n), do: :math.exp(n)
899+
900+
def exp(n) when is_number(n) do
901+
:math.exp(n)
902+
rescue
903+
ArithmeticError -> :infinity
904+
end
886905

887906
def exp(%Complex{re: :neg_infinity, im: _}), do: new(0, 0)
888907
def exp(%Complex{re: :infinity, im: :nan}), do: new(:infinity, :nan)
@@ -1126,7 +1145,16 @@ defmodule Complex do
11261145
def pow(_, :infinity), do: :infinity
11271146

11281147
def pow(x, y) when is_integer(x) and is_integer(y) and y >= 0, do: Integer.pow(x, y)
1129-
def pow(x, y) when is_number(x) and is_number(y), do: :math.pow(x, y)
1148+
1149+
def pow(x, y) when is_number(x) and is_number(y) do
1150+
:math.pow(x, y)
1151+
rescue
1152+
ArithmeticError ->
1153+
cond do
1154+
x == 0 and y < 0 -> :infinity
1155+
true -> :nan
1156+
end
1157+
end
11301158

11311159
def pow(x, y) do
11321160
x = as_complex(x)
@@ -1243,7 +1271,11 @@ defmodule Complex do
12431271
@spec asin(t | number | non_finite_number) :: t | number | non_finite_number
12441272
def asin(z)
12451273

1246-
def asin(n) when is_number(n), do: :math.asin(n)
1274+
def asin(n) when is_number(n) do
1275+
:math.asin(n)
1276+
rescue
1277+
ArithmeticError -> :nan
1278+
end
12471279

12481280
def asin(n) when is_non_finite_number(n), do: :nan
12491281

@@ -1317,7 +1349,11 @@ defmodule Complex do
13171349
@spec acos(t | number | non_finite_number) :: t | number | non_finite_number
13181350
def acos(z)
13191351

1320-
def acos(n) when is_number(n), do: :math.acos(n)
1352+
def acos(n) when is_number(n) do
1353+
:math.acos(n)
1354+
rescue
1355+
ArithmeticError -> :nan
1356+
end
13211357

13221358
def acos(n) when is_non_finite_number(n), do: :nan
13231359

@@ -1452,7 +1488,11 @@ defmodule Complex do
14521488
@spec cot(t | number | non_finite_number) :: t | number | non_finite_number
14531489
def cot(z)
14541490

1455-
def cot(n) when is_number(n), do: 1 / :math.tan(n)
1491+
def cot(n) when is_number(n) do
1492+
1 / :math.tan(n)
1493+
rescue
1494+
ArithmeticError -> :infinity
1495+
end
14561496

14571497
def cot(z) do
14581498
divide(cos(z), sin(z))
@@ -1478,7 +1518,11 @@ defmodule Complex do
14781518
@spec acot(t | number | non_finite_number) :: t | number | non_finite_number
14791519
def acot(z)
14801520

1481-
def acot(n) when is_number(n), do: :math.atan(1 / n)
1521+
def acot(n) when is_number(n) do
1522+
:math.atan(1 / n)
1523+
rescue
1524+
ArithmeticError -> :math.pi() / 2
1525+
end
14821526

14831527
def acot(:infinity), do: 0
14841528
def acot(:neg_infinity), do: :math.pi()
@@ -1592,7 +1636,13 @@ defmodule Complex do
15921636
@spec acsc(t | number | non_finite_number) :: t | number | non_finite_number
15931637
def acsc(z)
15941638

1595-
def acsc(n) when is_number(n), do: :math.asin(1 / n)
1639+
def acsc(n) when is_number(n) do
1640+
:math.asin(1 / n)
1641+
rescue
1642+
ArithmeticError ->
1643+
if n == 0, do: :infinity, else: :nan
1644+
end
1645+
15961646
def acsc(:infinity), do: 0
15971647
def acsc(:neg_infinity), do: -:math.pi()
15981648
def acsc(:nan), do: :nan
@@ -1630,7 +1680,11 @@ defmodule Complex do
16301680

16311681
def sinh(n) when is_non_finite_number(n), do: n
16321682

1633-
def sinh(n) when is_number(n), do: :math.sinh(n)
1683+
def sinh(n) when is_number(n) do
1684+
:math.sinh(n)
1685+
rescue
1686+
ArithmeticError -> if n > 0, do: :infinity, else: :neg_infinity
1687+
end
16341688

16351689
def sinh(z = %Complex{}) do
16361690
%Complex{re: re, im: im} =
@@ -1703,7 +1757,12 @@ defmodule Complex do
17031757
def cosh(:infinity), do: :infinity
17041758
def cosh(:neg_infinity), do: :infinity
17051759
def cosh(:nan), do: :nan
1706-
def cosh(n) when is_number(n), do: :math.cosh(n)
1760+
1761+
def cosh(n) when is_number(n) do
1762+
:math.cosh(n)
1763+
rescue
1764+
ArithmeticError -> :infinity
1765+
end
17071766

17081767
def cosh(z) do
17091768
%Complex{re: re, im: im} =
@@ -1732,10 +1791,16 @@ defmodule Complex do
17321791
def acosh(z)
17331792

17341793
if math_fun_supported?.(:acosh, 1) do
1735-
def acosh(n) when is_number(n), do: :math.acosh(n)
1794+
def acosh(n) when is_number(n) do
1795+
:math.acosh(n)
1796+
rescue
1797+
ArithmeticError -> :nan
1798+
end
17361799
else
17371800
def acosh(n) when is_number(n) do
17381801
:math.log(n + :math.sqrt(n * n - 1))
1802+
rescue
1803+
ArithmeticError -> :nan
17391804
end
17401805
end
17411806

@@ -1798,11 +1863,22 @@ defmodule Complex do
17981863
@spec atanh(t | number | non_finite_number) :: t | number | non_finite_number
17991864
def atanh(z)
18001865

1866+
def atanh(1), do: :infinity
1867+
def atanh(1.0), do: :infinity
1868+
def atanh(-1), do: :neg_infinity
1869+
def atanh(-1.0), do: :neg_infinity
1870+
18011871
if math_fun_supported?.(:atanh, 1) do
1802-
def atanh(n) when is_number(n), do: :math.atanh(n)
1872+
def atanh(n) when is_number(n) do
1873+
:math.atanh(n)
1874+
rescue
1875+
ArithmeticError -> :nan
1876+
end
18031877
else
18041878
def atanh(n) when is_number(n) do
18051879
0.5 * :math.log((1 + n) / (1 - n))
1880+
rescue
1881+
ArithmeticError -> :nan
18061882
end
18071883
end
18081884

test/complex_test.exs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,4 +1017,188 @@ defmodule ComplexTest do
10171017
""")
10181018
end
10191019
end
1020+
1021+
# ── IEEE 754 compliance ──────────────────────────────────────────
1022+
1023+
describe "IEEE 754: overflow returns Inf instead of crashing" do
1024+
test "exp(large) returns :infinity" do
1025+
assert Complex.exp(1000) == :infinity
1026+
assert Complex.exp(1000.0) == :infinity
1027+
end
1028+
1029+
test "sinh(large positive) returns :infinity" do
1030+
assert Complex.sinh(1000) == :infinity
1031+
assert Complex.sinh(1000.0) == :infinity
1032+
end
1033+
1034+
test "sinh(large negative) returns :neg_infinity" do
1035+
assert Complex.sinh(-1000) == :neg_infinity
1036+
assert Complex.sinh(-1000.0) == :neg_infinity
1037+
end
1038+
1039+
test "cosh(large) returns :infinity" do
1040+
assert Complex.cosh(1000) == :infinity
1041+
assert Complex.cosh(1000.0) == :infinity
1042+
end
1043+
end
1044+
1045+
describe "IEEE 754: domain errors return NaN instead of crashing" do
1046+
test "asin outside [-1, 1]" do
1047+
assert Complex.asin(2.0) == :nan
1048+
assert Complex.asin(-2.0) == :nan
1049+
end
1050+
1051+
test "acos outside [-1, 1]" do
1052+
assert Complex.acos(2.0) == :nan
1053+
assert Complex.acos(-2.0) == :nan
1054+
end
1055+
1056+
test "acosh below 1" do
1057+
assert Complex.acosh(0.5) == :nan
1058+
assert Complex.acosh(-1.0) == :nan
1059+
end
1060+
1061+
test "atanh outside (-1, 1)" do
1062+
assert Complex.atanh(2.0) == :nan
1063+
assert Complex.atanh(-2.0) == :nan
1064+
end
1065+
1066+
test "atanh at boundaries" do
1067+
assert Complex.atanh(1.0) == :infinity
1068+
assert Complex.atanh(1) == :infinity
1069+
assert Complex.atanh(-1.0) == :neg_infinity
1070+
assert Complex.atanh(-1) == :neg_infinity
1071+
end
1072+
end
1073+
1074+
describe "IEEE 754: division by zero" do
1075+
test "positive / 0.0 = :infinity" do
1076+
assert Complex.divide(1.0, 0.0) == :infinity
1077+
assert Complex.divide(5, 0.0) == :infinity
1078+
end
1079+
1080+
test "negative / 0.0 = :neg_infinity" do
1081+
assert Complex.divide(-1.0, 0.0) == :neg_infinity
1082+
assert Complex.divide(-5, 0.0) == :neg_infinity
1083+
end
1084+
1085+
test "0.0 / 0.0 = :nan" do
1086+
assert Complex.divide(0.0, 0.0) == :nan
1087+
assert Complex.divide(0, 0.0) == :nan
1088+
end
1089+
1090+
test "positive / -0.0 = :neg_infinity" do
1091+
assert Complex.divide(1.0, -0.0) == :neg_infinity
1092+
end
1093+
1094+
test "negative / -0.0 = :infinity" do
1095+
assert Complex.divide(-1.0, -0.0) == :infinity
1096+
end
1097+
1098+
test "normal division still works" do
1099+
assert Complex.divide(6.0, 3.0) == 2.0
1100+
assert Complex.divide(10, 2) == 5
1101+
end
1102+
end
1103+
1104+
describe "IEEE 754: sqrt domain errors" do
1105+
test "sqrt(negative) returns :nan" do
1106+
assert Complex.sqrt(-1) == :nan
1107+
assert Complex.sqrt(-1.0) == :nan
1108+
assert Complex.sqrt(-4.0) == :nan
1109+
end
1110+
end
1111+
1112+
describe "IEEE 754: pow edge cases" do
1113+
test "pow(0, negative) returns :infinity" do
1114+
assert Complex.pow(0, -1) == :infinity
1115+
assert Complex.pow(0.0, -1.0) == :infinity
1116+
end
1117+
end
1118+
1119+
describe "IEEE 754: cot/acot/acsc domain errors" do
1120+
test "cot(0) returns :infinity (1/tan(0) = 1/0)" do
1121+
assert Complex.cot(0) == :infinity
1122+
assert Complex.cot(0.0) == :infinity
1123+
end
1124+
1125+
test "acot(0) returns pi/2" do
1126+
assert_close Complex.acot(0), :math.pi() / 2
1127+
assert_close Complex.acot(0.0), :math.pi() / 2
1128+
end
1129+
1130+
test "acsc(0) returns :infinity" do
1131+
assert Complex.acsc(0) == :infinity
1132+
assert Complex.acsc(0.0) == :infinity
1133+
end
1134+
1135+
test "acsc(0.5) returns :nan (asin(2) domain error)" do
1136+
assert Complex.acsc(0.5) == :nan
1137+
end
1138+
end
1139+
1140+
describe "IEEE 754: integer division by zero" do
1141+
test "divide(1, 0) returns :infinity" do
1142+
assert Complex.divide(1, 0) == :infinity
1143+
end
1144+
1145+
test "divide(-1, 0) returns :neg_infinity" do
1146+
assert Complex.divide(-1, 0) == :neg_infinity
1147+
end
1148+
1149+
test "divide(0, 0) returns :nan" do
1150+
assert Complex.divide(0, 0) == :nan
1151+
end
1152+
end
1153+
1154+
describe "IEEE 754: pow domain errors" do
1155+
test "pow(-1, 0.5) returns :nan (sqrt of negative)" do
1156+
assert Complex.pow(-1, 0.5) == :nan
1157+
assert Complex.pow(-1.0, 0.5) == :nan
1158+
end
1159+
end
1160+
1161+
describe "IEEE 754: log edge cases" do
1162+
test "log(0) returns :neg_infinity" do
1163+
assert Complex.log(0) == :neg_infinity
1164+
assert Complex.log(0.0) == :neg_infinity
1165+
end
1166+
1167+
test "log(negative) returns :nan" do
1168+
assert Complex.log(-1.0) == :nan
1169+
end
1170+
1171+
test "log10(0) returns :neg_infinity" do
1172+
assert Complex.log10(0) == :neg_infinity
1173+
end
1174+
1175+
test "log2(0) returns :neg_infinity" do
1176+
assert Complex.log2(0) == :neg_infinity
1177+
end
1178+
end
1179+
1180+
describe "IEEE 754: tanh at extremes" do
1181+
test "tanh(large) clamps to 1/-1" do
1182+
assert Complex.tanh(1000) == 1.0
1183+
assert Complex.tanh(-1000) == -1.0
1184+
end
1185+
end
1186+
1187+
describe "IEEE 754: normal values still work" do
1188+
test "exp(0) == 1" do
1189+
assert Complex.exp(0) == 1.0
1190+
end
1191+
1192+
test "asin(0.5) is correct" do
1193+
assert_close Complex.asin(0.5), :math.asin(0.5)
1194+
end
1195+
1196+
test "sinh(1) is correct" do
1197+
assert_close Complex.sinh(1.0), :math.sinh(1.0)
1198+
end
1199+
1200+
test "cosh(1) is correct" do
1201+
assert_close Complex.cosh(1.0), :math.cosh(1.0)
1202+
end
1203+
end
10201204
end

0 commit comments

Comments
 (0)