Repository: sympy/sympy
Pull request: Fix: Return None in finite predicate Mul expression when a finite argument’s nonzero status is indeterminate
## Fixes #27447

#### Brief description of what is fixed or changed
Previously, in a Mul expression, if any argument was unbounded (infinite), the result would immediately be False, ignoring cases where a finite argument could be zero. However, if the finite argument is zero, the correct result should be None (undefined).

This PR accounts for that case and ensures the correct behavior by returning None when the finite argument’s value cannot be determined as nonzero.

#### Release Notes

<!-- BEGIN RELEASE NOTES -->
* assumptions
  * Make Q.finite(Mul(...)) return `None` when a finite argument’s nonzero status is indeterminate instead of `False`.

<!-- END RELEASE NOTES -->


Problem statement:
ask(Q.finite(f * i)) wrongly returns False (for finite f and infinite i)
```python
>>>f = Symbol('f', finite=True)
>>>i = Symbol('i', infinite=True)
>>>ask(Q.finite(f * i))
False
```

~~An infinite number times zero is zero as far as I'm aware. So since zero is a finite number, this behavior is incorrect.~~
An infinite number times zero is undefined, so the above behavior is incorrect.

Hints from issue discussion:
> An infinite number times zero is zero as far as I'm aware.

I would say that it is undefined. A useful thing to check is how sympy itself evaluates these things. The assumption system should be consistent with the evaluation rules. In this case:
```
In [1]: 0*oo
Out[1]: nan
```
Should this be considered a bug then? Should undefined values be considered to be neither finite nor infinite? Or should they be considered to possibly be finite or infinite? 


In other cases, SymPy seems to prefer the later interpretation that undefined values may be finite or infinite.
```python
>>>ask(Q.infinite(f * i)) is None
True
>>>S.NaN.is_finite is None
True
```
There exist tests for the old assumptions to ensure this sort of behavior (see [test_Mul_is_infinite](https://github.com/sympy/sympy/blob/d7938cbc2f703801413075c147f54db350d29bff/sympy/core/tests/test_assumptions.py#L927)).
Any assumptions query on something undefined should return None.
https://github.com/sympy/sympy/blob/bc83192924727faf4cd6bd21cec8cde39d0604f2/sympy/assumptions/handlers/calculus.py#L153-L167

I think the issue happens as It returns `False` as soon as it encounters any non-finite factor, without considering the possibility that the other factor might be zero.

#### Fix: as we encounter any non-finite arg we can check until now if is there any finite arg that has the possibility of being zero and instantly return None.

Commit message: remove failing test from test-issue-27447. Make the separate xfail function for failing test issue 27662

Patch to review:
diff --git a/.mailmap b/.mailmap
index 46892edcb46f..614f0d5712e2 100644
--- a/.mailmap
+++ b/.mailmap
@@ -771,6 +771,7 @@ Jason Ross <jasonross1024@gmail.com>
 Jason Siefken <siefkenj@gmail.com>
 Jason Tokayer <jason.tokayer@gmail.com>
 Jason Tokayer <jason.tokayer@gmail.com> <jason.tokayer@capitalone.com>
+Jatin Bhardwaj <bhardwajjatin093@gmail.com> <148186488+Jatinbhardwaj-093@users.noreply.github.com>
 Jatin Yadav <jatinyadav25@gmail.com>
 Javed Nissar <javednissar@gmail.com>
 Jay Patankar <patankarjays@gmail.com> Jay-Patankar <107458263+Jay-Patankar@users.noreply.github.com>
diff --git a/sympy/assumptions/handlers/calculus.py b/sympy/assumptions/handlers/calculus.py
index 263bed6da00c..40820ae4b169 100644
--- a/sympy/assumptions/handlers/calculus.py
+++ b/sympy/assumptions/handlers/calculus.py
@@ -151,10 +151,14 @@ def _(expr, assumptions):
         * /s = not signed
     """
     result = True
+    possible_zero = False
     for arg in expr.args:
         _bounded = ask(Q.finite(arg), assumptions)
         if _bounded:
-            continue
+            if ask(Q.zero(arg), assumptions) is not False:
+                if result is False:
+                    return None
+                possible_zero = None
         elif _bounded is None:
             if result is None:
                 return None
@@ -163,6 +167,8 @@ def _(expr, assumptions):
             if result is not False:
                 result = None
         else:
+            if possible_zero is None:
+                return None
             result = False
     return result
 
diff --git a/sympy/assumptions/tests/test_query.py b/sympy/assumptions/tests/test_query.py
index 14f1d88c944c..06384198f088 100644
--- a/sympy/assumptions/tests/test_query.py
+++ b/sympy/assumptions/tests/test_query.py
@@ -1019,9 +1019,9 @@ def test_bounded():
     a = x*y
     x, y = a.args
     assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is True
-    assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is False
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y)) is False
     assert ask(Q.finite(a), Q.finite(x)) is None
-    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)) is False
+    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) &~Q.zero(y)) is False
     assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is False
     assert ask(Q.finite(a), ~Q.finite(x)) is None
     assert ask(Q.finite(a), Q.finite(y)) is None
@@ -1031,24 +1031,24 @@ def test_bounded():
     x, y, z = a.args
     assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)
         & Q.finite(z)) is True
-    assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)
-        & ~Q.finite(z)) is False
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & Q.finite(y)
+        & ~Q.zero(y) & ~Q.finite(z)) is False
     assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is None
-    assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)
-        & Q.finite(z)) is False
-    assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y)
+        & Q.finite(z) & ~Q.zero(z)) is False
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y)
         & ~Q.finite(z)) is False
     assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is None
     assert ask(Q.finite(a), Q.finite(x) & Q.finite(z)) is None
     assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(z)) is None
     assert ask(Q.finite(a), Q.finite(x)) is None
-    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)
-        & Q.finite(z)) is False
-    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)
-        & ~Q.finite(z)) is False
+    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) & ~Q.zero(y)
+        & Q.finite(z) & ~Q.zero(z)) is False
+    assert ask(Q.finite(a), ~Q.finite(x) & ~Q.zero(x) & Q.finite(y)
+        & ~Q.zero(y) & ~Q.finite(z)) is False
     assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)) is None
     assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)
-        & Q.finite(z)) is False
+        & Q.finite(z) & ~Q.zero(z)) is False
     assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)
         & ~Q.finite(z)) is False
     assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is None
@@ -1112,6 +1112,32 @@ def test_issue_27441():
     assert ask(Q.composite(y), Q.integer(y) & Q.positive(y) & ~Q.prime(y)) is None
 
 
+def test_issue_27447():
+    x,y,z = symbols('x y z')
+    a = x*y
+    assert ask(Q.finite(a), Q.finite(x)  & ~Q.finite(y)) is None
+    assert ask(Q.finite(a), ~Q.finite(x)  & Q.finite(y)) is None
+
+    a = x*y*z
+    assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)
+        & ~Q.finite(z)) is None
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)
+        & Q.finite(z) ) is None
+    assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)
+        & ~Q.finite(z)) is None
+    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)
+        & Q.finite(z)) is None
+    assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)
+        & ~Q.finite(z)) is None
+    assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)
+        & Q.finite(z)) is None
+
+@XFAIL
+def test_issue_27662_xfail():
+    assert ask(Q.finite(x*y), ~Q.finite(x)
+        & Q.zero(y)) is None
+
+
 @XFAIL
 def test_bounded_xfail():
     """We need to support relations in ask for this to work"""


Code review comments:
diff_hunk: @@ -151,10 +151,14 @@ def _(expr, assumptions):
         * /s = not signed
     """
     result = True
+    possible_zero = False
     for arg in expr.args:
         _bounded = ask(Q.finite(arg), assumptions)
         if _bounded:
-            continue
+            if ask(Q.zero(arg), assumptions) is not False:
+                if result is False:
+                    return None
+                possible_zero = None
path: sympy/assumptions/handlers/calculus.py
text: @user1:
I think it would make more sense to just set possible_zero to True