Repository: pyccel/pyccel (python)

Issue:
Function call in if condition printed in wrong location
Regarding your interesting example:
```python
def f(k : int):
    a = {1:1.0, 2:2.0}
    b = {1:True, 2: False}
    if b.pop(k):
        return a.pop(k)
    else:
        return 3.0
```
It generates the following C function:
```c
double f(int64_t k)
{
    hmap_int64_t_double a = {0};
    hmap_int64_t_bool b = {0};
    double Dummy_0000;
    bool Dummy_0001;
    double Out_0001;
    a = c_init(hmap_int64_t_double, {{INT64_C(1), 5.0}, {INT64_C(2), 6.0}});
    b = c_init(hmap_int64_t_bool, {{INT64_C(1), 1}, {INT64_C(2), 0}});
    if (Dummy_0001)
    {
        Dummy_0000 = hmap_int64_t_double_pop(&a, k);
        Out_0001 = Dummy_0000;
        hmap_int64_t_double_drop(&a);
        hmap_int64_t_bool_drop(&b);
        return Out_0001;
    }
    else
    {
        Dummy_0001 = hmap_int64_t_bool_pop(&b, k); // <-- PROBLEM
        hmap_int64_t_double_drop(&a);
        hmap_int64_t_bool_drop(&b);
        return 3.0;
    }
    hmap_int64_t_double_drop(&a);
    hmap_int64_t_bool_drop(&b);
}
```
Beside the 'header guard' issue that I fixed, I think there is another issue here - `hmap_int64_t_bool_pop(&b, k)` is evaluated inside the `else` block instead of being computed before the conditional statement. This results in incorrect behavior, as the conditional logic depends on the value of `b.pop(k)` but evaluates it at the wrong point.

This issue is not specific to the `dict.pop` case. It also occurs in other cases, such as:
```python
def test_min():
    if min(5,6):
        return 1
    else:
        return 0
```
Which generates the following C code:
```c
int64_t test_min(void)
{
    int64_t Dummy_0000;
    int64_t Dummy_0001;
    if (((Dummy_0000 < Dummy_0001 ? Dummy_0000 : Dummy_0001) != 0))
    {
        return INT64_C(1);
    }
    else
    {
        Dummy_0000 = INT64_C(5); // <-- PROBLEM
        Dummy_0001 = INT64_C(6);
        return INT64_C(0);
    }
}
```
Here, the variables `Dummy_0000` and `Dummy_0001` used in the `min` function are only evaluated in the `else` block, leading to incorrect behavior because the condition depends on their values.
It seems that this issue occurs specifically within `if-else` blocks, as `if` blocks alone appear to function correctly. 
Since its not specific to `dict.pop`, resolving it may fall outside the scope of this PR, but it should certainly be addressed in future work. Let me know what you think :smile:

_Originally posted by @emmanuel-ferdman in https://github.com/pyccel/pyccel/issues/2143#issuecomment-2587577755_
            

Pull request description:
Fix use of `_additional_code` in If condition
Fix use of `_additional_code` in If condition. Fixes #2149. Additionally avoid creating variables to hold literals in min/max functions.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e4d4845..2a074e48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,10 +101,11 @@ All notable changes to this project will be documented in this file.
 -   #2111 : Fix declaration of class attributes with name conflicts using type annotations.
 -   #2115 : Fix integer handling with NumPy 2.0 on Windows.
 -   Fix handling of union `typing.TypeAlias` objects as type hints.
--   #2141 : Fix error when removing `test_node`
+-   #2141 : Fix error when removing `test_node`.
 -   #2148 : Fix error due to missing file `numpy_version.h`.
 -   #2001 : Ensure all memory is correctly deallocated in the Python interface in a way that is compatible with all compilers.
 -   #2153 : Fix missing line information when an unknown class method is called.
+-   #2149 : Fix multi-line expressions in `if` conditions.
 
 ### Changed
 
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index c8e1e0fb..d00a7275 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -772,20 +772,22 @@ class CCodePrinter(CodePrinter):
             arg2 = self._print(arg[1])
             return f"f{expr.name}({arg1}, {arg2})"
         elif arg.dtype.primitive_type is PrimitiveIntegerType() and len(arg) == 2:
-            if isinstance(arg[0], Variable):
+            if isinstance(arg[0], (Variable, Literal)):
                 arg1 = self._print(arg[0])
             else:
                 arg1_temp = self.scope.get_temporary_variable(PythonNativeInt())
                 assign1 = Assign(arg1_temp, arg[0])
-                self._additional_code += self._print(assign1)
+                code = self._print(assign1)
+                self._additional_code += code
                 arg1 = self._print(arg1_temp)
 
-            if isinstance(arg[1], Variable):
+            if isinstance(arg[1], (Variable, Literal)):
                 arg2 = self._print(arg[1])
             else:
                 arg2_temp = self.scope.get_temporary_variable(PythonNativeInt())
                 assign2 = Assign(arg2_temp, arg[1])
-                self._additional_code += self._print(assign2)
+                code = self._print(assign2)
+                self._additional_code += code
                 arg2 = self._print(arg2_temp)
 
             op = '<' if isinstance(expr, PythonMin) else '>'
@@ -952,16 +954,26 @@ class CCodePrinter(CodePrinter):
 
     def _print_If(self, expr):
         lines = []
-        for i, (c, e) in enumerate(expr.blocks):
-            var = self._print(e)
-            if i == 0:
-                lines.append("if (%s)\n{\n" % self._print(c))
-            elif i == len(expr.blocks) - 1 and isinstance(c, LiteralTrue):
-                lines.append("else\n{\n")
+        condition_setup = []
+        for i, (c, b) in enumerate(expr.blocks):
+            body = self._print(b)
+            if i == len(expr.blocks) - 1 and isinstance(c, LiteralTrue):
+                lines.append("else\n")
             else:
-                lines.append("else if (%s)\n{\n" % self._print(c))
-            lines.append("%s}\n" % var)
-        return "".join(lines)
+                # Print condition
+                condition = self._print(c)
+                # Retrieve any additional code which cannot be executed in the line containing the condition
+                condition_setup.append(self._additional_code)
+                self._additional_code = ''
+                # Add the condition to the lines of code
+                line = f"if ({condition})\n"
+                if i == 0:
+                    lines.append(line)
+                else:
+                    lines.append("else " + line)
+            lines.append("{\n")
+            lines.append(body + "}\n")
+        return "".join(chain(condition_setup, lines))
 
     def _print_IfTernaryOperator(self, expr):
         cond = self._print(expr.cond)
@@ -1266,7 +1278,8 @@ class CCodePrinter(CodePrinter):
                 tmp_arg_format_list = CStringExpression(', ').join(tmp_arg_format_list)
                 args_format.append(CStringExpression('(', tmp_arg_format_list, ')'))
                 assign = Assign(tmp_list, f)
-                self._additional_code += self._print(assign)
+                code = self._print(assign)
+                self._additional_code += code
             elif f.rank > 0 and not isinstance(f.class_type, StringType):
                 if args_format:
                     code += formatted_args_to_printf(args_format, args, sep)
@@ -2213,7 +2226,8 @@ class CCodePrinter(CodePrinter):
                 elif not self.is_c_pointer(arg_val):
                     tmp_var = self.scope.get_temporary_variable(f.dtype)
                     assign = Assign(tmp_var, arg_val)
-                    self._additional_code += self._print(assign)
+                    code = self._print(assign)
+                    self._additional_code += code
                     args.append(ObjectAddress(tmp_var))
                 else:
                     args.append(arg_val)
@@ -2457,10 +2471,6 @@ class CCodePrinter(CodePrinter):
             for_code = self._additional_code + for_code
             self._additional_code = ''
 
-        if self._additional_code:
-            for_code = self._additional_code + for_code
-            self._additional_code = ''
-
         body = self._print(additional_assign) + self._print(expr.body)
 
         self.exit_scope()


Test patch:
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index bb861d1a..54de6b57 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -122,6 +122,32 @@ def test_min_3_args(language):
     assert epyc_f(*int_args) == f(*int_args)
     assert np.isclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
 
+def test_min_if(language):
+    def f(x : 'int', y : 'int'):
+        if min(x+x+y, x+y+y) < (x+y):
+            return x+y
+        else:
+            return x-y
+
+    epyc_f = epyccel(f, language=language)
+
+    int_args = [randint(min_int//3, max_int//3) for _ in range(2)]
+
+    assert epyc_f(*int_args) == f(*int_args)
+
+def test_min_in_min(language):
+    def f(x : 'int', y : 'int'):
+        if min(min(x+x,+y), min(x+y,y)) < (x+y): #pylint: disable=nested-min-max
+            return x+y
+        else:
+            return x-y
+
+    epyc_f = epyccel(f, language=language)
+
+    int_args = [randint(min_int//3, max_int//3) for _ in range(2)]
+
+    assert epyc_f(*int_args) == f(*int_args)
+
 @pytest.mark.parametrize( 'language', (
         pytest.param("fortran", marks = pytest.mark.fortran),
         pytest.param("c", marks = [