Repository: pyccel/pyccel (python)

Issue:
Add compile time analysis of if blocks
### Relevant Discussion

Discussed in meeting 

### Describe the feature
If an `if` block has a condition which is always `True` then subsequent `elif` blocks can result in warnings from the compiler about inaccessible code. We would like to use the functions in `ast/sympy_helper.py` to check if a condition is always true or always false.
`SemanticParser._visit_If` should then filter the `IfSection`s based on if the condition is `LiteralTrue`, `LiteralFalse` or a runtime condition discarding inaccessible code. This may result in the code from one if block being returned directly.

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def f(a : 'int[:] | int[:,:]'):
    if len(a.shape) == 1:
        print("rank one")
        print(a[0])
    else:
        print("rank two")
        print(a[0,0])
```

### Proposed Solution
- [ ] use `pyccel_to_sympy` to change `IfSection` conditions to `LiteralTrue`/`LiteralFalse` where appropriate
- [x] modify `SemanticParser._visit_If` to filter IfSections

Pull request description:
Add compile time analysis of if blocks
Add compile time analysis of if block conditions to allow differentiating code based on the type or rank. Fixes #2183 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56a2a832..690a7ff8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,7 @@ All notable changes to this project will be documented in this file.
 -   #983 : Add support for built-in function `round`.
 -   Add support for `type` as a type annotation.
 -   #2182 : Add support for `isinstance`.
+-   #2183 : Add compile time analysis of if block conditions.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.
diff --git a/pyccel/ast/sympy_helper.py b/pyccel/ast/sympy_helper.py
index f2c80cdf..b249c974 100644
--- a/pyccel/ast/sympy_helper.py
+++ b/pyccel/ast/sympy_helper.py
@@ -17,9 +17,12 @@ from .builtins  import PythonRange, PythonTuple
 from .datatypes import PrimitiveIntegerType
 from .internals import PyccelArrayShapeElement
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex
+from .literals  import LiteralTrue, LiteralFalse
 from .mathext   import MathCeil
 from .operators import PyccelAdd, PyccelMul, PyccelPow, PyccelUnarySub
 from .operators import PyccelDiv, PyccelMinus, PyccelAssociativeParenthesis
+from .operators import PyccelEq, PyccelNe, PyccelLt, PyccelLe, PyccelGt, PyccelGe
+from .operators import PyccelAnd, PyccelOr, PyccelNot
 from .variable  import Variable
 
 __all__ = ('pyccel_to_sympy',
@@ -168,6 +171,42 @@ def pyccel_to_sympy(expr, symbol_map, used_names):
         args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
         return args[0] + args[1]
 
+    elif isinstance(expr, PyccelEq):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return sp.Eq(args[0], args[1])
+
+    elif isinstance(expr, PyccelNe):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return sp.Ne(args[0], args[1])
+
+    elif isinstance(expr, PyccelLe):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return args[0] <= args[1]
+
+    elif isinstance(expr, PyccelLt):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return args[0] < args[1]
+
+    elif isinstance(expr, PyccelGe):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return args[0] >= args[1]
+
+    elif isinstance(expr, PyccelGt):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return args[0] > args[1]
+
+    elif isinstance(expr, PyccelAnd):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return sp.And(*args)
+
+    elif isinstance(expr, PyccelOr):
+        args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
+        return sp.Or(*args)
+
+    elif isinstance(expr, PyccelNot):
+        arg = pyccel_to_sympy(expr.args[0], symbol_map, used_names)
+        return sp.Not(arg)
+
     elif isinstance(expr, PyccelPow):
         args = [pyccel_to_sympy(e, symbol_map, used_names) for e in expr.args]
         return args[0] ** args[1]
@@ -203,6 +242,12 @@ def pyccel_to_sympy(expr, symbol_map, used_names):
         args = [pyccel_to_sympy(a, symbol_map, used_names) for a in expr]
         return sp.Tuple(*args)
 
+    elif isinstance(expr, LiteralTrue):
+        return sp.logic.boolalg.BooleanTrue()
+
+    elif isinstance(expr, LiteralFalse):
+        return sp.logic.boolalg.BooleanFalse()
+
     elif isinstance(expr, (sp.core.basic.Atom, sp.core.operations.AssocOp, sp.Set)):
         # Already translated
         return expr
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 2ca4f663..b2ac66bf 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -16,6 +16,8 @@ from sympy.utilities.iterables import iterable as sympy_iterable
 from sympy import Sum as Summation
 from sympy import Symbol as sp_Symbol
 from sympy import Integer as sp_Integer
+from sympy.logic.boolalg import BooleanTrue as sp_True
+from sympy.logic.boolalg import BooleanFalse as sp_False
 from sympy import ceiling
 
 from textx.exceptions import TextXSyntaxError
@@ -4037,14 +4039,34 @@ class SemanticParser(BasicParser):
 
         cond = self._visit(expr.condition)
 
+        symbol_map = {}
+        used_names = self.scope.all_used_symbols.copy()
+        try:
+            sympy_cond = pyccel_to_sympy(cond, symbol_map, used_names)
+        except TypeError:
+            sympy_cond = 'unknown'
+
+        if sympy_cond == sp_False():
+            return IfSection(LiteralFalse(), CodeBlock([]))
+        elif sympy_cond == sp_True():
+            cond = LiteralTrue()
+
         body = self._visit(expr.body)
 
         return IfSection(cond, body)
 
     def _visit_If(self, expr):
-        args = [self._visit(i) for i in expr.blocks]
+        args = []
 
-        conds = [b.condition for b in args]
+        for b in expr.blocks:
+            new_b = self._visit(b)
+            cond = new_b.condition
+            if not isinstance(cond, LiteralFalse):
+                args.append(new_b)
+            if isinstance(cond, LiteralTrue):
+                if len(args) == 1:
+                    return new_b.body
+                break
 
         allocations = [arg.get_attribute_nodes(Allocate) for arg in args]
 


Test patch:
diff --git a/tests/epyccel/test_epyccel_compile_time_if.py b/tests/epyccel/test_epyccel_compile_time_if.py
new file mode 100644
index 00000000..6da58eb2
--- /dev/null
+++ b/tests/epyccel/test_epyccel_compile_time_if.py
@@ -0,0 +1,75 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+import numpy as np
+import pytest
+from pyccel import epyccel
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_rank_differentiation_1(language):
+    def f(a : 'int[:] | int[:,:]'):
+        if len(a.shape) == 1:
+            return a[0]
+        else:
+            return a[0,0]
+
+    x = np.arange(10, dtype=int)
+    y = np.array(np.reshape(x[::-1], (2,5)), dtype=int)
+
+    f_epyc = epyccel(f, language=language)
+    assert f_epyc(x) == f(x)
+    assert f_epyc(y) == f(y)
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_rank_differentiation_2(language):
+    def f(a : 'int[:] | int[:,:]'):
+        if len(a.shape) != 2:
+            return a[0]
+        else:
+            return a[0,0]
+
+    x = np.arange(10, dtype=int)
+    y = np.array(np.reshape(x[::-1], (2,5)), dtype=int)
+
+    f_epyc = epyccel(f, language=language)
+    assert f_epyc(x) == f(x)
+    assert f_epyc(y) == f(y)
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_type_differentiation(language):
+    def f(a : 'int | float'):
+        if isinstance(a, int):
+            return a*2
+        else:
+            return -a
+
+    f_epyc = epyccel(f, language=language)
+    assert f_epyc(3) == f(3)
+    assert isinstance(f_epyc(3), type(f(3)))
+    assert f_epyc(4.0) == f(4.0)
+    assert isinstance(f_epyc(4.0), type(f(4.0)))