Repository: pyccel/pyccel (python)

Issue:
[SETS] Add C support for len function
### Describe the feature
In Python the len function can be called on many functions however the implementation of this function is different in different languages. Support should be added to test the length of a list in C (and Python if it does not work out of the box).

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def f():
    a = {1,2,1}
    b = len(a)
    return b
```

### Proposed Solution
This feature should be implemented using the functionalities provided by STC

Pull request:
Add C and Fortran support for `len()` function
Add C and Fortran support for the `len()` function for `list` and `set` using the equivalent `container_X_size()` from the `STC` library and ` % size()` from the gFTL library. This PR fixes issues #1874 and #1875.

**Commit Summary**
- Reorganise CHANGELOG to group related points
- Update the `PythonLen` docstring. 
- Update `FortranCodePrinter._print_PyccelArrayShapeElement` method to print ` % size()` for containers.
- Update `CCodePrinter._print_PyccelArrayShapeElement` method to check whether the element passed is a `list` or `set`, and then print their equivalent using the `STC`.
- Update `shape_can_change` method to return `True` only for `HomogeneousListType` and `HomogeneousSetType` objects as their shape can change.
- Add tests for the `len()` function using `list` and `set` with different data types.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1568b1d3..9f51ca31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,37 +6,40 @@ All notable changes to this project will be documented in this file.
 ### Added
 
 -   #1720 : Add support for `Ellipsis` as the only index for an array.
+-   #1787 : Ensure STC is installed with Pyccel.
+-   #1656 : Ensure gFTL is installed with Pyccel.
 -   #1694 : Add Python support for list method `extend()`.
 -   #1700 : Add Python support for list method `sort()`.
 -   #1696 : Add Python support for list method `copy()`.
 -   #1693 : Add Python support for list method `remove()`.
--   #1690 : Add C support for list method `pop()`.
--   #1739 : Add abstract class `SetMethod` to handle calls to various set methods.
 -   #1739 : Add Python support for set method `clear()`.
 -   #1740 : Add Python support for set method `copy()`.
 -   #1750 : Add Python support for set method `remove()`.
 -   #1743 : Add Python support for set method `discard()`.
 -   #1754 : Add Python support for set method `update()`.
 -   #1893 : Add Python support for set initialisation with `set()`.
--   #1787 : Ensure STC is installed with Pyccel.
--   #1656 : Ensure gFTL is installed with Pyccel.
--   #1844 : Add line numbers and code to errors from built-in function calls.
--   #1655 : Add the appropriate C language equivalent for declaring a Python `list` container using the STC library.
--   #1657 : Add the appropriate Fortran language equivalent for declaring a Python `list` container using the gFTL library.
--   #1876 : Add C support for indexing lists.
--   #1659 : Add the appropriate C language equivalent for declaring a Python `set` container using the STC library.
--   #1877 : Add C Support for set method `pop()`.
 -   #1895 : Add Python support for dict initialisation with `{}`.
 -   #1895 : Add Python support for dict initialisation with `dict()`.
 -   #1886 : Add Python support for dict method `pop()`.
 -   #1887 : Add Python support for dict method `popitem()`.
 -   #1885 : Add Python support for dict method `get()`.
+-   #1844 : Add line numbers and code to errors from built-in function calls.
+-   #1655 : Add the appropriate C language equivalent for declaring a Python `list` container using the STC library.
+-   #1659 : Add the appropriate C language equivalent for declaring a Python `set` container using the STC library.
 -   #1944 : Add the appropriate C language equivalent for declaring a Python `dict` container using the STC library.
+-   #1657 : Add the appropriate Fortran language equivalent for declaring a Python `list` container using the gFTL library.
+-   #1874 : Add C and Fortran support for the `len()` function for the `list` container.
+-   #1875 : Add C and Fortran support for the `len()` function for the `set` container.
+-   #1908 : Add C and Fortran support for the `len()` function for the `dict` container.
+-   #1876 : Add C support for indexing lists.
+-   #1690 : Add C support for list method `pop()`.
+-   #1877 : Add C Support for set method `pop()`.
 -   #1936 : Add missing C output for inline decorator example in documentation
 -   #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
 -   #1544 : Add support for `typing.TypeAlias`.
 -   #1583 : Allow inhomogeneous tuples in classes.
 -   Add a warning about containers in lists.
+-   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[DEVELOPER\] Added an improved traceback to the developer-mode errors for errors in function calls.
 
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index fb05aae7..9c3cead5 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -622,6 +622,10 @@ class PythonLen(PyccelFunction):
     be calculated in the generated code, but in an inhomogeneous object
     the integer value of the shape must be returned.
 
+    If the shape is unknown and cannot be determined at compile time then
+    the first element of the shape is a `PyccelArrayShapeElement` which
+    can be used to generate the equivalent C code using the `STC` library.
+
     Parameters
     ----------
     arg : TypedAstNode
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 346a1883..039a49e9 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -35,9 +35,9 @@ from pyccel.ast.core import Assign, AliasAssign, Declare, Deallocate
 from pyccel.ast.core import FunctionCall, PyccelFunctionDef
 
 from pyccel.ast.datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
-from pyccel.ast.datatypes import SymbolicType, StringType, FixedSizeNumericType
-from pyccel.ast.datatypes import PythonNativeInt, HomogeneousTupleType, HomogeneousListType
-from pyccel.ast.datatypes import HomogeneousSetType, DictType, HomogeneousContainerType
+from pyccel.ast.datatypes import SymbolicType, StringType, FixedSizeNumericType, HomogeneousContainerType
+from pyccel.ast.datatypes import HomogeneousTupleType, HomogeneousListType, HomogeneousSetType, DictType
+from pyccel.ast.datatypes import PythonNativeInt
 from pyccel.ast.datatypes import CustomDataType, InhomogeneousTupleType, TupleType
 from pyccel.ast.datatypes import pyccel_type_to_original_type, PyccelType
 
@@ -1409,20 +1409,26 @@ class FCodePrinter(CodePrinter):
         return f'size({init_value}, kind={prec})'
 
     def _print_PyccelArrayShapeElement(self, expr):
-        init_value = self._print(expr.arg)
+        arg = expr.arg
+        arg_code = self._print(arg)
         prec = self.print_kind(expr)
 
-        if expr.arg.rank == 1:
-            return f'size({init_value}, kind={prec})'
+        if isinstance(arg.class_type, (NumpyNDArrayType, HomogeneousTupleType)):
+            if arg.rank == 1:
+                return f'size({arg_code}, kind={prec})'
 
-        if expr.arg.order == 'C':
-            index = PyccelMinus(LiteralInteger(expr.arg.rank), expr.index, simplify = True)
-            index = self._print(index)
-        else:
-            index = PyccelAdd(expr.index, LiteralInteger(1), simplify = True)
-            index = self._print(index)
+            if arg.order == 'C':
+                index = PyccelMinus(LiteralInteger(arg.rank), expr.index, simplify = True)
+                index = self._print(index)
+            else:
+                index = PyccelAdd(expr.index, LiteralInteger(1), simplify = True)
+                index = self._print(index)
 
-        return f'size({init_value}, {index}, {prec})'
+            return f'size({arg_code}, {index}, {prec})'
+        elif isinstance(arg.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
+            return f'{arg_code} % size()'
+        else:
+            raise NotImplementedError(f"Don't know how to represent shape of object of type {arg.class_type}")
 
     def _print_PythonInt(self, expr):
         value = self._print(expr.arg)
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 2dcc14e0..d5b3cdc7 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -13,10 +13,10 @@ from pyccel.ast.core       import CodeBlock, Import, Assign, FunctionCall, For,
 from pyccel.ast.core       import IfSection, FunctionDef, Module, PyccelFunctionDef
 from pyccel.ast.datatypes  import HomogeneousTupleType, HomogeneousListType
 from pyccel.ast.functionalexpr import FunctionalFor
-from pyccel.ast.literals   import LiteralTrue, LiteralString
+from pyccel.ast.literals   import LiteralTrue, LiteralString, LiteralInteger
 from pyccel.ast.numpyext   import numpy_target_swap
 from pyccel.ast.numpyext   import NumpyArray, NumpyNonZero, NumpyResultType
-from pyccel.ast.numpytypes import NumpyNumericType
+from pyccel.ast.numpytypes import NumpyNumericType, NumpyNDArrayType
 from pyccel.ast.variable   import DottedName, Variable
 from pyccel.ast.utilities  import builtin_import_registry as pyccel_builtin_import_registry
 from pyccel.ast.utilities  import decorators_mod
@@ -490,10 +490,18 @@ class PythonCodePrinter(CodePrinter):
         return 'print({})\n'.format(', '.join(self._print(a) for a in expr.expr))
 
     def _print_PyccelArrayShapeElement(self, expr):
-        arg = self._print(expr.arg)
-        index = self._print(expr.index)
-        name = self._get_numpy_name(expr)
-        return f'{name}({arg})[{index}]'
+        arg = expr.arg
+        index = expr.index
+        arg_code = self._print(arg)
+        if isinstance(arg.class_type, (NumpyNDArrayType, HomogeneousTupleType)) or \
+                not isinstance(index, LiteralInteger):
+            index_code = self._print(index)
+            name = self._get_numpy_name(expr)
+            return f'{name}({arg_code})[{index_code}]'
+        elif index == 0:
+            return f'len({arg_code})'
+        else:
+            raise NotImplementedError("The shape access function seems to be poorly defined.")
 
     def _print_PyccelArraySize(self, expr):
         arg = self._print(expr.arg)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 79144b69..9d8da36a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -21,6 +21,15 @@ if github_debugging:
 def language(request):
     return request.param
 
+@pytest.fixture( params=[
+        pytest.param("c", marks = pytest.mark.c),
+        pytest.param("python", marks = pytest.mark.python),
+    ],
+    scope = "session"
+)
+def stc_language(request):
+    return request.param
+
 def move_coverage(path_dir):
     for root, _, files in os.walk(path_dir):
         for name in files:
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index bf3f583b..d9c3ec26 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -369,3 +369,71 @@ def test_len_inhomog_tuple(language):
     epyc_f = epyccel(f, language=language)
 
     assert epyc_f() == f()
+
+def test_len_list_int(language):
+    def f():
+        a = [1, 2, 3]
+        return len(a)
+
+    epyc_f = epyccel(f, language=language)
+
+    assert epyc_f() == f()
+
+def test_len_list_float(language):
+    def f():
+        a = [1.4, 2.6, 3.5]
+        b = len(a)
+        return b
+
+    epyc_f = epyccel(f, language=language)
+
+    assert epyc_f() == f()
+
+def test_len_list_complex(language):
+    def f():
+        a = [1j, 2 + 1j, 3 + 1j]
+        b = len(a)
+        return b
+
+    epyc_f = epyccel(f, language=language)
+
+    assert epyc_f() == f()
+
+def test_len_set_int(stc_language):
+    def f():
+        a = {1, 2, 3}
+        return len(a)
+
+    epyc_f = epyccel(f, language=stc_language)
+
+    assert epyc_f() == f()
+
+def test_len_set_float(stc_language):
+    def f():
+        a = {1.4, 2.6, 3.5}
+        b = len(a)
+        return b
+
+    epyc_f = epyccel(f, language=stc_language)
+
+    assert epyc_f() == f()
+
+def test_len_set_complex(stc_language):
+    def f():
+        a = {1j, 2 + 1j, 3 + 1j}
+        b = len(a)
+        return b
+
+    epyc_f = epyccel(f, language=stc_language)
+
+    assert epyc_f() == f()
+
+def test_len_dict_int_float(stc_language):
+    def f():
+        a = {1:1.0, 2:2.0, 3:3.0, 4:4.0}
+        b = len(a)
+        return b
+
+    epyc_f = epyccel(f, language=stc_language)
+
+    assert epyc_f() == f()
diff --git a/tests/epyccel/test_epyccel_lists.py b/tests/epyccel/test_epyccel_lists.py
index 75ff809f..024d53ad 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -17,18 +17,6 @@ from pyccel import epyccel
 def language(request):
     return request.param
 
-@pytest.fixture( params=[
-        pytest.param("c", marks = pytest.mark.c),
-        pytest.param("fortran", marks = [
-            pytest.mark.skip(reason="lists not implemented in fortran"),
-            pytest.mark.fortran]),
-        pytest.param("python", marks = pytest.mark.python)
-    ],
-    scope = "module"
-)
-def stc_language(request):
-    return request.param
-
 def test_pop_last_element(stc_language) :
     def pop_last_element():
         a = [1,3,45]
diff --git a/tests/epyccel/test_epyccel_variable_annotations.py b/tests/epyccel/test_epyccel_variable_annotations.py
index 5ec88ccf..1ea48ceb 100644
--- a/tests/epyccel/test_epyccel_variable_annotations.py
+++ b/tests/epyccel/test_epyccel_variable_annotations.py
@@ -10,18 +10,6 @@ from pyccel import epyccel
 from pyccel.errors.errors import PyccelSemanticError
 from pyccel.decorators import allow_negative_index, stack_array
 
-@pytest.fixture( params=[
-        pytest.param("c", marks = pytest.mark.c),
-        pytest.param("fortran", marks = [
-            pytest.mark.xfail(reason="Variable declaration not implemented in fortran, related issues #1657 1658"),
-            pytest.mark.fortran]),
-        pytest.param("python", marks = pytest.mark.python)
-    ],
-    scope = "module"
-)
-def stc_language(request):
-    return request.param
-
 def test_local_type_annotation(language):
     def local_type_annotation():
         gift : int