Repository: apytypes/apytypes (language: python)

Issue:
Behaviour of scalar fixed-point division by zero
We need to determine a coherent behavior for scalar fixed-point division by zero. Currently, the Python interpreter crashes on fixed-point division by zero if the resulting `bit` attribute is smaller than or equal to one limb (`floating point exception (core dumped)`). The interpreter hangs indefinitely if the resulting division `bit` attribute is greater than one limb.

I suggest we raise a [`ZeroDivisionError`](https://docs.python.org/3/library/exceptions.html#ZeroDivisionError) on scalar fixed-point division by zero. For `APyFixedArray`, I believe the best option is to say it is undefined, only to guarantee that the program does not crash or hang indefinitely. Anything else is going to make SIMD execution very difficult.

## Behaviour of other Python types
```python3
>>> 1.0 / 0.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero
```

```python3
>>> import numpy as np
>>> np.array([1.0, 1.0]) / np.array([0.0, 0.0])
RuntimeWarning: divide by zero encountered in divide
array([inf, inf])
```

```python3
>>> np.array([1.0, 1.0]) // np.array([0.0, 0.0])
RuntimeWarning: divide by zero encountered in floor_divide
array([inf, inf])
```

```python3
>>> np.array([1, 1]) // np.array([0, 0])
array([0, 0])
```


Discussion / hints:
Raising `ZeroDivisionError` from nanobind question: https://github.com/wjakob/nanobind/discussions/567

Fix patch:
diff --git a/src/apyfixed.cc b/src/apyfixed.cc
index a2239d0ed..65a1f7b78 100644
--- a/src/apyfixed.cc
+++ b/src/apyfixed.cc
@@ -201,8 +201,8 @@ APyFixed APyFixed::operator*(const APyFixed& rhs) const
 APyFixed APyFixed::operator/(const APyFixed& rhs) const
 {
     if (rhs.is_zero()) {
-        // TODO: Raise a Python `ZeroDivisionError` here when possible
-        throw nb::value_error("fixed-point division by zero");
+        PyErr_SetString(PyExc_ZeroDivisionError, "fixed-point division by zero");
+        throw nb::python_error();
     }
 
     const int res_int_bits = int_bits() + rhs.frac_bits() + 1;


Test patch:
diff --git a/lib/test/apyfixed/test_arithmetic.py b/lib/test/apyfixed/test_arithmetic.py
index 62872483d..3df747e48 100644
--- a/lib/test/apyfixed/test_arithmetic.py
+++ b/lib/test/apyfixed/test_arithmetic.py
@@ -18,6 +18,10 @@ def test_arithmetic_operations():
     assert (a / b).is_identical(APyFixed.from_float(-1.65625, int_bits=6, frac_bits=6))
     assert (b / a).is_identical(APyFixed.from_float(-0.59375, int_bits=7, frac_bits=5))
 
+    # Division-by-zero throws a ZeroDivisionError
+    with pytest.raises(ZeroDivisionError, match="fixed-point division by zero"):
+        _ = APyFixed.from_float(1.0, 10, 10) / APyFixed.from_float(0.0, 10, 10)
+
 
 def test_overflow_twos_complement():
     assert float(APyFixed(1 << 127, 1, 127)) == -1.0