Repository: pyccel/pyccel
Pull request #1728 (2024-02-13 20:36:18)

Annotate functions when they are called
This PR solves the issue [#1720](https://github.com/pyccel/pyccel/issues/1720).
- Add support for `Ellipsis` as the only index for an array.
- Error raised when incompatible arguments are passed to an `inlined` function is now fatal.
- `FunctionDef` is annotated when it is called, or at the end of the `CodeBlock` if it is never called.
- `InlinedFunctionDef` is only annotated if it is called.
- `InlinedFunctionDef`  is never printed except in the Python printer.

Now we can run the following examples without errors
```python
def f():
    print(1)
    g()

def g():
    print(2)
    
f()
```
or like this
```python
def f():
    def g():
        return x
    x = 1
    return g()

x = f()
```
We also can compile the following example in  `0.64 seconds` instead of `6.4 seconds`, so 10 times faster
```python
@inline
@template(name='T',types=['int8[:]','int8[:,:]','int8[:,:,:]','int8[:,:,:,:]','int8[:,:,:,:,:]','int8[:,:,:,:,:,:]',\
                           'int16[:]','int16[:,:]','int16[:,:,:]','int16[:,:,:,:]','int16[:,:,:,:,:]','int16[:,:,:,:,:,:]',\
                           'int32[:]','int32[:,:]','int32[:,:,:]','int32[:,:,:,:]','int32[:,:,:,:,:]','int32[:,:,:,:,:,:]',\
                           'int64[:]','int64[:,:]','int64[:,:,:]','int64[:,:,:,:]','int64[:,:,:,:,:]','int64[:,:,:,:,:,:]',\
                           'float32[:]','float32[:,:]','float32[:,:,:]','float32[:,:,:,:]','float32[:,:,:,:,:]','float32[:,:,:,:,:,:]',\
                           'float64[:]','float64[:,:]','float64[:,:,:]','float64[:,:,:,:]','float64[:,:,:,:,:]','float64[:,:,:,:,:,:]'])
def add(a : 'T', b : 'T', c:'T'):
	c[...] = a+b

a1 = np.ones(10)
b1 = np.ones(10)
c1 = np.zeros(10)

a2 = np.ones((10,10))
b2 = np.ones((10,10))
c2 = np.zeros((10,10))

add(a1, b1, c1)
add(a2, b2, c2)

pyccel-bot[bot]: Hello! Welcome to Pyccel! Thank you very much for your contribution :heart:.

I am the GitHub bot. I will help guide you through the different stages necessary to validate a review in Pyccel. If you haven't yet seen our developer docs make sure you check them out [here](https://github.com/pyccel/pyccel/tree/master/developer_docs). Amongst other things they describe the [review process](https://github.com/pyccel/pyccel/blob/master/developer_docs/review_process.md) that we have just started. You can also get in touch with our other developers on our [Pyccel Discord Server](https://discord.gg/2Q6hwjfFVb).

To begin with I will give you a short checklist to make sure your pull request is complete. Please tick items off when you have completed them or determined that they are not necessary for this pull request. If you want me to run any specific tests to check out corner cases that you can't easily check on your computer, you can request this using the command `/bot run X`. Use the command `/bot show tests` to see a full list of the tests I can run. Once you have finished preparing your pull request and are ready to request reviews just take your PR out of draft, or let me know with the command `/bot mark as ready`. I will then run the full suite of tests to check that everything is as neat as you think before asking other contributors for reviews. Tests will not run automatically before this point to avoid wasting resources. You can get a full list of commands that I understand using `/bot commands`.

Please begin by requesting your checklist using the command `/bot checklist`

said-hadjout: /bot show tests
pyccel-bot[bot]: The following is a list of keywords which can be used to run tests. Tests in bold are run by pull requests when they are marked as ready for review:
- **linux** : Runs the unit tests on a Linux system.
- **windows** : Runs the unit tests on a Windows system.
- **macosx** : Runs the unit tests on a MacOS X system.
- **coverage** : Runs the unit tests on a Linux system and checks the coverage of the tests.
- **docs** : Checks if the documentation follows the numpydoc format.
- **pylint** : Runs pylint on files which are too big to be handled by codacy.
- **pyccel_lint** : Runs a linter to check that Pyccel's best practices are followed.
- **spelling** : Checks if everything in the documentation is spelled (and capitalised) correctly.
- pr_tests : Runs all the tests marked in bold.
- pickle : Checks that .pyccel files have been correctly generated and installed by the installation process.
- editable_pickle : Checks that .pyccel files have been correctly generated and installed by the editable installation process.
- pickle_wheel : Checks that .pyccel files have been correctly generated and packaged into the wheel.
- anaconda_linux : Runs the unit tests on a linux system using anaconda for python.
- anaconda_windows : Runs the unit tests on a windows system using anaconda for python.
- intel : Runs the unit tests on a linux system using the intel compiler.

These tests can be run with the command `/bot run X` (multiple tests can be specified separated by spaces), or with `try V X` to test on Python version V.

said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux windows macosx
pyccel-bot[bot]: I can't seem to find your checklist to confirm that you have completed all necessary tasks. Please request one using `/bot checklist`.

said-hadjout: /bot checklist
pyccel-bot[bot]: It seems like you have forgotten to complete your checklist for this pull request. Please check the list [here](https://github.com/pyccel/pyccel/pull/1728#issuecomment-1949508035), fix the missing items and let me know when everything is ticked and fixed with `/bot mark as ready`.

said-hadjout: /bot mark as ready
pyccel-bot[bot]: It seems like you have forgotten to complete your checklist for this pull request. Please check the list [here](https://github.com/pyccel/pyccel/pull/1728#issuecomment-1949508035), fix the missing items and let me know when everything is ticked and fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

said-hadjout: /bot run linux
said-hadjout: /bot run linux windows macosx
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

said-hadjout: /bot run pylint coverage
said-hadjout: /bot run pylint coverage
said-hadjout: /bot run windows macosx
said-hadjout: /bot run pylint coverage linux
said-hadjout: /bot run pylint coverage linux
saidctb: /bot run pylint coverage linux
saidctb: /bot run pylint
saidctb: /bot run pylint coverage linux
saidctb: /bot run pylint coverage linux
saidctb: /bot run pylint coverage linux
saidctb: /bot run pylint coverage linux
saidctb: /bot run windows macosx docs pyccel_lint spelling
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

EmilyBourne: @said-hadjout don't worry about requesting reviews and adding labels the bot will take care of this when the PR is ready for review
saidctb: /bot run docs 
saidctb: /bot run docs
saidctb: /bot run pylint coverage linux windows macosx docs pyccel_lint spelling
saidctb: /bot run  codacy
pyccel-bot[bot]: There are unrecognised tests.
The following is a list of keywords which can be used to run tests. Tests in bold are run by pull requests when they are marked as ready for review:
- **linux** : Runs the unit tests on a Linux system.
- **windows** : Runs the unit tests on a Windows system.
- **macosx** : Runs the unit tests on a MacOS X system.
- **coverage** : Runs the unit tests on a Linux system and checks the coverage of the tests.
- **docs** : Checks if the documentation follows the numpydoc format.
- **pylint** : Runs pylint on files which are too big to be handled by codacy.
- **pyccel_lint** : Runs a linter to check that Pyccel's best practices are followed.
- **spelling** : Checks if everything in the documentation is spelled (and capitalised) correctly.
- pr_tests : Runs all the tests marked in bold.
- pickle : Checks that .pyccel files have been correctly generated and installed by the installation process.
- editable_pickle : Checks that .pyccel files have been correctly generated and installed by the editable installation process.
- pickle_wheel : Checks that .pyccel files have been correctly generated and packaged into the wheel.
- anaconda_linux : Runs the unit tests on a linux system using anaconda for python.
- anaconda_windows : Runs the unit tests on a windows system using anaconda for python.
- intel : Runs the unit tests on a linux system using the intel compiler.

These tests can be run with the command `/bot run X` (multiple tests can be specified separated by spaces), or with `try V X` to test on Python version V.

EmilyBourne: I have no idea what happened to Codacy. Sometimes it goes stops collecting PRs for a bit (we're trying to move away from it). It doesn't even seem to be able to find your PR anymore. 
saidctb: > I have no idea what happened to Codacy. Sometimes it goes stops collecting PRs for a bit (we're trying to move away from it). It doesn't even seem to be able to find your PR anymore.

I remember it always being like that, sometimes it stops for no reason 
EmilyBourne: > > I have no idea what happened to Codacy. Sometimes it goes stops collecting PRs for a bit (we're trying to move away from it). It doesn't even seem to be able to find your PR anymore.
> 
> I remember it always being like that, sometimes it stops for no reason

Sometimes it gets saturated and will start again at night but this time it seems slightly more stuck:
![image](https://github.com/pyccel/pyccel/assets/25363473/7b244a52-3351-4c2c-9980-090212557f9f)

I have contacted support.
pyccel-bot[bot]: Hey @pyccel/pyccel-dev ! @said-hadjout has just created this great new pull request! Check it out and let me know what you think!

pyccel-bot[bot]: @said-hadjout, @EmilyBourne has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

said-hadjout: > Thanks for this PR. This looks like quite a lot of work but it looks really useful. I'm afraid I have quite a lot of comments/questions though.
> 
> Does this PR fix #551 ?
> 
> You mention :
> 
> > I made some non-blocking errors blocking in this [function](https://github.com/pyccel/pyccel/blob/d9bf2a422a9d87a03759cc6504d55d84f85cebaf/pyccel/parser/semantic.py#L1043) otherwise pyccel will crash if the function is not annotated.
> 
> Can you explain more about why this would be the case and add this change to the CHANGELOG please.

For the issue #551 , no it does not fix it, I don't think it is related.
As for the non-blocking errors, if the call arguments are incompatible with the function arguments, the code will crash afterwards, because the functiondef is not annotated only when it is called with compatible arguments, by not making the errors blocking, it code will crash in the `_visit_FunctionCall`
said-hadjout: \bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run linux
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run linux
said-hadjout: /bot run docs
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run pr_tests
said-hadjout: /bot run docs
said-hadjout: /bot run docs
said-hadjout: /bot run pr_tests
said-hadjout: /bot run macosx
saidctb: @EmilyBourne can you take a look at this PR
EmilyBourne: > @EmilyBourne can you take a look at this PR

As per the instructions in the bot message. Feel free to mark the PR as Ready for review again once you think you have tackled everything mentioned. I will have a look when I can (either this afternoon or maybe Tuesday evening). I think the PR is a lot cleaner now but that means I should probably go over everything again so I'd like to make sure I have a free 2 hour window before starting
saidctb: /bot mark as ready
pyccel-bot[bot]: @EmilyBourne, @said-hadjout has been working hard and thinks that they have now replied to or fixed all your comments. Could you take another look at the PR and see if you can approve now?

pyccel-bot[bot]: @said-hadjout, @EmilyBourne has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

saidctb: /bot run linux
saidctb: /bot run linux
saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready/bot mark as ready
pyccel-bot[bot]: @EmilyBourne, @said-hadjout has been working hard and thinks that they have now replied to or fixed all your comments. Could you take another look at the PR and see if you can approve now?

pyccel-bot[bot]: @said-hadjout, @EmilyBourne has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

saidctb: /bot run linux
saidctb: /bot run linux
saidctb: /bot run linux
EmilyBourne: /bot run linux
EmilyBourne: I cancelled and retriggered your linux test as before the merge your test will fail due to a pytest update. The last PR fixed this problem
saidctb: /bot mark as ready
saidctb: /bot run pr_tests
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot run docs
saidctb: /bot run docs
saidctb: /bot run docs
saidctb: /bot mark as ready
saidctb: /bot run windows
saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

saidctb: /bot mark as ready
pyccel-bot[bot]: @EmilyBourne, @said-hadjout has been working hard and thinks that they have now replied to or fixed all your comments. Could you take another look at the PR and see if you can approve now?

pyccel-bot[bot]: @said-hadjout, @EmilyBourne has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

saidctb: /bot mark as ready
saidctb: /bot run pr_tests
saidctb: /bot mark as ready
pyccel-bot[bot]: @EmilyBourne, @said-hadjout has been working hard and thinks that they have now replied to or fixed all your comments. Could you take another look at the PR and see if you can approve now?

saidctb: @yguclu , can you please merge this PR? it is ready to be merged 
saidctb: Are we still going to merge this PR or is it blocked by something?
saidctb: > > Are we still going to merge this PR or is it blocked by something?
> 
> It is not blocked by anything other than time constraints. Bigger PRs always take longer to be reviewed a sufficient number of times. Yaman was at a workshop last week so he had no time to look at PRs. It will be merged you just need to be a bit patient. Is it urgent for some reason?

no, it is not urgent, I thought that it was ready to merge, which means that it does not need any other review 
EmilyBourne: > > > Are we still going to merge this PR or is it blocked by something?
> > 
> > 
> > It is not blocked by anything other than time constraints. Bigger PRs always take longer to be reviewed a sufficient number of times. Yaman was at a workshop last week so he had no time to look at PRs. It will be merged you just need to be a bit patient. Is it urgent for some reason?
> 
> no, it is not urgent, I thought that it was ready to merge, which means that it does not need any other review

No. The description of the "Ready to merge" label states:
> Approved by senior developer. Ready for final approval and merge

All PRs should be reviewed at least twice ideally 3 times. Once by a junior dev/intern (during the `needs-initial-review` stage, this can be skipped). Once by a senior dev, and once by whoever is merging (Yaman). The bigger the PR the more reviews it needs. E.g #1756 is huge so it needs reviewing by as many people as possible
saidctb: > Great job @said-hadjout!
> 
> I have posted some comments below, and I also have a more general request.
> 
> Apparently the argument `annotate` to the `_visit_FunctionDef` method of the semantic parser is only needed because the syntactic parser does not collect function and class definitions. It seems natural that we should take care of that in a smaller PR before merging this (big) PR. What do you think?
> 
> The [`_visit_Module`](https://github.com/pyccel/pyccel/blob/9183af9497bc30bac74a17e70462eb53b2331d32/pyccel/parser/syntactic.py#L340-L350) method of the syntactic parser returns
> 
> ```python
> return Module(name, [], [], program = CodeBlock(body), scope=self.scope)
> ```
> 
> while it could perform some basic operations:
> 
> ```python
> funcs   = [f for f in body if isinstance(f, FunctionDef)]
> classes = [c for c in body if isinstance(c, ClassDef)]
> imports = [i for i in body if isinstance(i, Import)]
> body    = [b for b in body if not isinstance(b, (FunctionDef, ClassDef, Import))]
> 
> module  = Module(name, [], funcs,
>     classes = classes,
>     imports = imports,
>     program = CodeBlock(body),
>     scope   = self.scope)
> 
> return module
> ```
> 
> @said-hadjout, @EmilyBourne Do you see any downsides to this approach?

It is doable but might generate many conflicts, so I prefer merging this one and fixing the issue in https://github.com/pyccel/pyccel/issues/1768 afterward.
Your suggestion does not include nested functions and nested classes (if we want to support them)
We also will need to add them to the scope in the semantics stage.
after that we will remove the whole `_visit_FunctionDef` method and replace it with a method called
`def treat_functiondef(...)` for example, so that it will not be visited automatically.
EmilyBourne: > Your suggestion does not include nested functions and nested classes (if we want to support them)

For nested functions/classes the solution would be the same just in a different function. The snippet is still sufficient to describe the behaviour
pyccel-bot[bot]: @said-hadjout, @yguclu has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

yguclu: > after that we will remove the whole `_visit_FunctionDef` method and replace it with a method called
> `def treat_functiondef(...)` for example, so that it will not be visited automatically.

Why is this necessary? After solving #1768 the `FunctionDef` should be visited always in the semantic stage, or not?
saidctb: > > after that we will remove the whole `_visit_FunctionDef` method and replace it with a method called
> > `def treat_functiondef(...)` for example, so that it will not be visited automatically.
> 
> Why is this necessary? After solving #1768 the `FunctionDef` should be visited always in the semantic stage, or not?

The FunctionDef will not be part of the AST because we removed it, so it shouldn't be visited using the `_visit(...)`
method, but it should be explicitly called by doing `treat_functiondef`
yguclu: > > > after that we will remove the whole `_visit_FunctionDef` method and replace it with a method called
> > > `def treat_functiondef(...)` for example, so that it will not be visited automatically.
> > 
> > 
> > Why is this necessary? After solving #1768 the `FunctionDef` should be visited always in the semantic stage, or not?
> 
> The FunctionDef will not be part of the AST because we removed it, so it shouldn't be visited using the `_visit(...)` method, but it should be explicitly called by calling `treat_functiondef`

Am I missing something? The `FunctionDef` object will be stored in a `Module` object (or possibly in another `FunctionDef` object), and therefore it will definitely be part of the AST.
saidctb: > > > > after that we will remove the whole `_visit_FunctionDef` method and replace it with a method called
> > > > `def treat_functiondef(...)` for example, so that it will not be visited automatically.
> > > 
> > > 
> > > Why is this necessary? After solving #1768 the `FunctionDef` should be visited always in the semantic stage, or not?
> > 
> > 
> > The FunctionDef will not be part of the AST because we removed it, so it shouldn't be visited using the `_visit(...)` method, but it should be explicitly called by calling `treat_functiondef`
> 
> Am I missing something? The `FunctionDef` object will be stored in a `Module` object (or possibly in another `FunctionDef` object), and therefore it will definitely be part of the AST.

Sorry, yes it is not clear what I'm saying, when I used the term AST, I'm talking about the body of the Module which is a tree, let take the Python example
```python
a = 1
def f():
    pass
b = 2
while cond:
    f()
```
when we fix the issue https://github.com/pyccel/pyccel/issues/1768
the result of the syntactic stage will give us this tree in the Module body
```python
Assign(a, 1)
EmptyLine()
Assign(b, 2)
While(cond, [FunctionCall(f, [])])
```
So we will never encounter a `FunctionDef` or at least we shouldn't because the function defs will be saved separately in the module attribute. That is why we would not need to include them in the visitor algorithm, we should instead raise an error if it is encountered, e.g. 
```python
for i in range(10):
    def g():
        print(i)
    g()
```
This should automatically raise an error, which we do not currently 
yguclu: All right, let's leave issue #1768 for later! Please do not forget my other comments.
saidctb: /bot run pr_tests
saidctb: We have a weird error in ´test_add_literal_int[python]´ 
```python
     def _print_SetAdd(self, expr):
        name = self._print(expr.set_variable)
>       args = self._print(expr.add_argument)
E       AttributeError: 'SetAdd' object has no attribute 'add_argument'
```
the class `SetAdd` doesn't have that attribute or property
EmilyBourne: > We have a weird error in `test_add_literal_int[python]` in `pycode.py`
> 
> ```python
>      def _print_SetAdd(self, expr):
>         name = self._print(expr.set_variable)
> >       args = self._print(expr.add_argument)
> E       AttributeError: 'SetAdd' object has no attribute 'add_argument'
> ```
> 
> the class `SetAdd` doesn't have that attribute or property

This looks like a bad merge. This method no longer exists in the devel branch
saidctb: > > We have a weird error in `test_add_literal_int[python]` in `pycode.py`
> > ```python
> >      def _print_SetAdd(self, expr):
> >         name = self._print(expr.set_variable)
> > >       args = self._print(expr.add_argument)
> > E       AttributeError: 'SetAdd' object has no attribute 'add_argument'
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > the class `SetAdd` doesn't have that attribute or property
> 
> This looks like a bad merge. This method no longer exists in the devel branch

Yes, you are right
saidctb: /bot run pr_tests
saidctb: /bot mark as ready
saidctb: /bot run pr_tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de6d46ba..31901090 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,15 +4,21 @@ All notable changes to this project will be documented in this file.
 ## \[UNRELEASED\]
 
 ### Added
-
--   #1694 : Add Python support for list method `extend()`.    
+-   #1720 : Add support for `Ellipsis` as the only index for an array.
+-   #1694 : Add Python support for list method `extend()`.
 -   #1739 : Add Python support for set method `clear()`.
 -   #1739 : Add abstract class `SetMethod` to handle calls to various set methods.
 -   #1740 : Add Python support for set method `copy()`.
 
 ### Fixed
 
+-   #1720 : Fix Undefined Variable error when the function definition is after the variable declaration.
+
 ### Changed
+-   #1720 : functions with the `@inline` decorator are no longer exposed to Python in the shared library.
+-   #1720 : Error raised when incompatible arguments are passed to an `inlined` function is now fatal.
+-   \[INTERNALS\] `FunctionDef` is annotated when it is called, or at the end of the `CodeBlock` if it is never called.
+-   \[INTERNALS\] `InlinedFunctionDef` is only annotated if it is called.
 
 ### Deprecated
 
diff --git a/docs/decorators.md b/docs/decorators.md
index bf126c66..5fc1a4bd 100644
--- a/docs/decorators.md
+++ b/docs/decorators.md
@@ -364,6 +364,7 @@ end module boo
 ## Inline
 
 The `@inline` decorator indicates that the body of a function should be printed directly when it is called rather than passing through an additional function call. This can be useful for code optimisation.
+Any functions with the `@inline` decorator will not be exposed to the user in the shared library.
 
 ### Basic Example
 
@@ -598,6 +599,14 @@ The generated C code:
 ```c
 ```
 
+### Import Error when imported from the shared library
+Using the previous example, if we import the function `get_val`, we get this error:
+```
+Traceback (most recent call last):
+  File "<string>", line 1, in <module>
+ImportError: cannot import name 'get_val' from 'boo' (/home/__init__.py)
+```
+
 ## Getting Help
 
 If you face problems with Pyccel, please take the following steps:
diff --git a/pyccel/ast/basic.py b/pyccel/ast/basic.py
index fa38daac..e02cc66d 100644
--- a/pyccel/ast/basic.py
+++ b/pyccel/ast/basic.py
@@ -10,6 +10,7 @@ This module contains classes from which all pyccel nodes inherit. They are:
   AST nodes requiring type descriptors.
 """
 import ast
+from types import GeneratorType
 
 from pyccel.utilities.stage   import PyccelStage
 
@@ -35,7 +36,7 @@ def iterable(x):
     bool
         True if object is iterable for a PyccelAstNode.
     """
-    return isinstance(x, (list, tuple, dict_keys, dict_values, set))
+    return isinstance(x, (list, tuple, dict_keys, dict_values, set, GeneratorType))
 
 pyccel_stage = PyccelStage()
 
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index a752bc71..9dd1dc10 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -2277,6 +2277,14 @@ class FunctionDef(ScopedAstNode):
     is_external : bool
         True for a function which cannot be explicitly imported or renamed.
 
+    is_imported : bool, default : False
+        True for a function that is imported.
+
+    is_semantic : bool, optional
+        True for a function that is annotated.
+        It is used to indicate if the function has been visited in the semantic stage or not.
+        It is only used by the clone method, where we might clone a syntactic function in the semantic stage.
+
     functions : list, tuple
         A list of functions defined within this function.
 
@@ -2331,7 +2339,8 @@ class FunctionDef(ScopedAstNode):
                  '_decorators','_headers','_is_recursive','_is_pure',
                  '_is_elemental','_is_private','_is_header',
                  '_functions','_interfaces','_docstring', '_is_external',
-                 '_result_pointer_map')
+                 '_result_pointer_map','_is_imported', '_is_semantic')
+
     _attribute_nodes = ('_arguments','_results','_body',
                  '_global_vars','_imports','_functions','_interfaces')
 
@@ -2353,6 +2362,8 @@ class FunctionDef(ScopedAstNode):
         is_private=False,
         is_header=False,
         is_external=False,
+        is_imported=False,
+        is_semantic=None,
         functions=(),
         interfaces=(),
         result_pointer_map={},
@@ -2448,11 +2459,13 @@ class FunctionDef(ScopedAstNode):
         self._is_private      = is_private
         self._is_header       = is_header
         self._is_external     = is_external
+        self._is_imported     = is_imported
         self._functions       = functions
         self._interfaces      = interfaces
         self._result_pointer_map = result_pointer_map
         self._docstring      = docstring
         super().__init__(scope)
+        self._is_semantic    = self.pyccel_staging != 'syntactic' if is_semantic is None else is_semantic
 
     @property
     def name(self):
@@ -2596,6 +2609,15 @@ class FunctionDef(ScopedAstNode):
         from a f77 module """
         return self._is_external
 
+    @property
+    def is_imported(self):
+        """
+        Indicates if the function was imported from another file.
+
+        Indicates if the function was imported from another file.
+        """
+        return self._is_imported
+
     @property
     def is_inline(self):
         """ True if the function should be printed inline """
@@ -2610,6 +2632,16 @@ class FunctionDef(ScopedAstNode):
         """
         return self._is_static
 
+    @property
+    def is_semantic(self):
+        """
+        Indicates if the function was created with semantic information.
+
+        Indicates if the function has been annotated with type descriptors
+        in the semantic stage.
+        """
+        return self._is_semantic
+
     @property
     def functions(self):
         """ List of functions within this function """
@@ -2661,7 +2693,6 @@ class FunctionDef(ScopedAstNode):
         new_func.rename(newname)
         return new_func
 
-
     def rename(self, newname):
         """
         Rename the FunctionDef name
@@ -2700,6 +2731,8 @@ class FunctionDef(ScopedAstNode):
         'is_header':self._is_header,
         'functions':self._functions,
         'is_external':self._is_external,
+        'is_imported':self._is_imported,
+        'is_semantic':self._is_semantic,
         'interfaces':self._interfaces,
         'docstring':self._docstring,
         'scope':self._scope}
@@ -2756,19 +2789,27 @@ class InlineFunctionDef(FunctionDef):
     """
     Represents a function definition for an inline function.
 
+    Represents a function definition for an inline function.
+
     Parameters
     ----------
-    See FunctionDef
-
-    namespace_imports : Scope
-                        The objects in the scope which are available due to imports
+    *args : list
+        The FunctionDef class arguments.
+    namespace_imports : dict
+        The imports available in the function Scope.
+    global_funcs : iterable, optional
+        The global functions used in the function.
+    **kwargs : dict
+        The FunctionDef class keyword arguments.   
     """
     __slots__ = ('_namespace_imports','_orig_args','_new_args','_new_local_vars', '_if_block_replacements',
             '_global_funcs')
 
     def __init__(self, *args, namespace_imports = None, global_funcs = None, **kwargs):
+        if namespace_imports is not None:
+            assert isinstance(namespace_imports, dict)
         self._namespace_imports = namespace_imports
-        self._global_funcs = global_funcs
+        self._global_funcs = tuple(global_funcs) if global_funcs is not None else None
         super().__init__(*args, **kwargs)
         self._orig_args = tuple(a.var for a in self.arguments)
         self._new_args  = None
@@ -2861,6 +2902,15 @@ class InlineFunctionDef(FunctionDef):
         """ List of global functions used in the function """
         return self._global_funcs
 
+    def __getnewargs__(self):
+        """
+        This method returns the positional and keyword arguments
+        used to create an instance of this class.
+        """
+        args, kwargs = super().__getnewargs__()
+        kwargs.update({'namespace_imports':self._namespace_imports, 'global_funcs':self._global_funcs})
+        return args, kwargs
+
 class PyccelFunctionDef(FunctionDef):
     """
     Class used for storing `PyccelInternalFunction` objects in a FunctionDef.
@@ -2930,13 +2980,19 @@ class Interface(PyccelAstNode):
     is_argument : bool
         True if the interface is used for a function argument.
 
+    is_imported : bool
+        True if the interface is imported from another file.
+
+    syntactic_node : FunctionDef, default: None
+        The syntactic node that is not annotated.
+
     Examples
     --------
     >>> from pyccel.ast.core import Interface, FunctionDef
     >>> f = FunctionDef('F', [], [], [])
     >>> Interface('I', [f])
     """
-    __slots__ = ('_name','_functions','_is_argument')
+    __slots__ = ('_name','_functions','_is_argument', '_is_imported', '_syntactic_node')
     _attribute_nodes = ('_functions',)
 
     def __init__(
@@ -2944,15 +3000,20 @@ class Interface(PyccelAstNode):
         name,
         functions,
         is_argument = False,
+        is_imported=False,
+        syntactic_node=None,
         ):
 
         if not isinstance(name, str):
             raise TypeError('Expecting an str')
-        if not isinstance(functions, list):
-            raise TypeError('Expecting a list')
+
+        assert iterable(functions)
+
         self._name = name
-        self._functions = functions
+        self._functions = tuple(functions)
         self._is_argument = is_argument
+        self._is_imported = is_imported
+        self._syntactic_node = syntactic_node
         super().__init__()
 
     @property
@@ -2970,6 +3031,24 @@ class Interface(PyccelAstNode):
         """True if the interface is used for a function argument."""
         return self._is_argument
 
+    @property
+    def is_imported(self):
+        """
+        Indicates if the function was imported from another file.
+
+        Indicates if the function was imported from another file.
+        """
+        return self._is_imported
+
+    @property
+    def syntactic_node(self):
+        """
+        The syntactic node that is not annotated.
+
+        The syntactic node that is not annotated.
+        """
+        return self._syntactic_node
+
     @property
     def docstring(self):
         """
@@ -2979,6 +3058,83 @@ class Interface(PyccelAstNode):
         """
         return self._functions[0].docstring
 
+    @property
+    def is_semantic(self):
+        """
+         Flag to check if the node is annotated.
+
+         Flag to check if the node has been annotated with type descriptors
+         in the semantic stage.
+        """
+        return self._functions[0].is_semantic
+
+    @property
+    def is_inline(self):
+        """
+         Flag to check if the node is inlined.
+
+         Flag to check if the node is inlined.
+        """
+        return self._functions[0].is_inline
+
+    def rename(self, newname):
+        """
+        Rename the Interface name to a newname.
+
+        Rename the Interface name to a newname.
+
+        Parameters
+        ----------
+        newname : str
+            New name for the Interface.
+        """
+
+        self._name = newname
+
+    def clone(self, newname, **new_kwargs):
+        """
+        Create an almost identical Interface with name `newname`.
+
+        Create an almost identical Interface with name `newname`.
+        Additional parameters can be passed to alter the resulting
+        FunctionDef.
+
+        Parameters
+        ----------
+        newname : str
+            New name for the Interface.
+
+        **new_kwargs : dict
+            Any new keyword arguments to be passed to the new Interface.
+
+        Returns
+        -------
+        Interface
+            The clone of the interface.
+        """
+
+        args, kwargs = self.__getnewargs__()
+        kwargs.update(new_kwargs)
+        cls = type(self)
+        new_func = cls(*args, **kwargs)
+        new_func.rename(newname)
+        return new_func
+
+    def __getnewargs__(self):
+        """
+          This method returns the positional and keyword arguments
+            used to create an instance of this class.
+        """
+        args = (
+        self._name,
+        self._functions)
+
+        kwargs = {
+        'is_argument': self._is_argument,
+        'is_imported':self._is_imported,
+        'syntactic_node':self._syntactic_node}
+        return args, kwargs
+
     def point(self, args, use_final_precision = False):
         """Returns the actual function that will be called, depending on the passed arguments."""
         fs_args = [[j for j in i.arguments] for i in
diff --git a/pyccel/ast/utilities.py b/pyccel/ast/utilities.py
index 7876d5b1..c2b2daa2 100644
--- a/pyccel/ast/utilities.py
+++ b/pyccel/ast/utilities.py
@@ -22,7 +22,7 @@ from .cmathext      import cmath_mod
 from .datatypes     import NativeHomogeneousTuple
 from .internals     import PyccelInternalFunction, Slice
 from .itertoolsext  import itertools_mod
-from .literals      import LiteralInteger, Nil
+from .literals      import LiteralInteger, LiteralEllipsis, Nil
 from .mathext       import math_mod
 from .sysext        import sys_mod
 
@@ -251,22 +251,24 @@ def compatible_operation(*args, language_has_vectors = True):
 #==============================================================================
 def insert_index(expr, pos, index_var):
     """
-    Function to insert an index into an expression at a given position
+    Function to insert an index into an expression at a given position.
+
+    Function to insert an index into an expression at a given position.
 
     Parameters
-    ==========
-    expr        : Ast Node
-                The expression to be modified
-    pos         : int
-                The index at which the expression is modified
-                (If negative then there is no index to insert)
-    index_var   : Variable
-                The variable which will be used for indexing
+    ----------
+    expr : PyccelAstNode
+       The expression to be modified.
+    pos : int
+       The index at which the expression is modified
+       (If negative then there is no index to insert).
+    index_var : Variable
+       The variable which will be used for indexing.
 
     Returns
-    =======
-    expr        : Ast Node
-                Either a modified version of expr or expr itself
+    -------
+    PyccelAstNode
+       The modified version of expr.
 
     Examples
     --------
@@ -279,10 +281,8 @@ def insert_index(expr, pos, index_var):
     >>> i = Variable('int', 'i')
     >>> d = PyccelAdd(a,b)
     >>> expr = Assign(c,d)
-    >>> insert_index(expr, 0, i, language_has_vectors = False)
+    >>> insert_index(expr, 0, i)
     IndexedElement(c, i) := IndexedElement(a, i) + IndexedElement(b, i)
-    >>> insert_index(expr, 0, i, language_has_vectors = True)
-    c := a + b
     """
     if expr.rank==0:
         return expr
@@ -313,16 +313,18 @@ def insert_index(expr, pos, index_var):
     elif isinstance(expr, IndexedElement):
         base = expr.base
         indices = list(expr.indices)
+        if len(indices) == 1 and isinstance(indices[0], LiteralEllipsis):
+            indices = [Slice(None,None)]*base.rank
         i = -1
-        while i>=pos and -i<=expr.base.rank:
+        while i>=pos and -i<=base.rank:
             if not isinstance(indices[i], Slice):
                 pos -= 1
             i -= 1
-        if -pos>expr.base.rank:
+        if -pos>base.rank:
             return expr
 
         # Add index at the required position
-        if expr.base.shape[pos]==1:
+        if base.shape[pos]==1:
             # If there is no dimension in this axis, reduce the rank
             assert(indices[pos].start is None)
             index_var = LiteralInteger(0)
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 3e48bd5e..9800233a 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -18,7 +18,7 @@ from .datatypes import (datatype, DataType,
                         NativeComplex, NativeHomogeneousTuple, NativeInhomogeneousTuple)
 from .internals import PyccelArrayShapeElement, Slice, get_final_precision, PyccelSymbol
 from .internals import apply_pickle
-from .literals  import LiteralInteger, Nil
+from .literals  import LiteralInteger, Nil, LiteralEllipsis
 from .operators import (PyccelMinus, PyccelDiv, PyccelMul,
                         PyccelUnarySub, PyccelAdd)
 
@@ -863,7 +863,7 @@ class IndexedElement(TypedAstNode):
     True
     """
     __slots__ = ('_label', '_indices','_dtype','_precision','_shape','_rank','_order','_class_type')
-    _attribute_nodes = ('_label', '_indices')
+    _attribute_nodes = ('_label', '_indices', '_shape')
 
     def __init__(self, base, *indices):
 
@@ -871,7 +871,7 @@ class IndexedElement(TypedAstNode):
             raise IndexError('Indexed needs at least one index.')
 
         self._label = base
-
+        self._shape = None
         if pyccel_stage == 'syntactic':
             self._indices = indices
             super().__init__()
@@ -883,16 +883,19 @@ class IndexedElement(TypedAstNode):
         shape = base.shape
         rank  = base.rank
 
-        # Add empty slices to fully index the object
-        if len(indices) < rank:
-            indices = indices + tuple([Slice(None, None)]*(rank-len(indices)))
-
-        if any(not isinstance(a, (int, TypedAstNode, Slice)) for a in indices):
+        if any(not isinstance(a, (int, TypedAstNode, Slice, LiteralEllipsis)) for a in indices):
             errors.report("Index is not of valid type",
                     symbol = indices, severity = 'fatal')
 
-        self._indices = tuple(LiteralInteger(a) if isinstance(a, int) else a for a in indices)
-        super().__init__()
+        if len(indices) == 1 and isinstance(indices[0], LiteralEllipsis):
+            self._indices = tuple(LiteralInteger(a) if isinstance(a, int) else a for a in indices)
+            indices = [Slice(None,None)]*rank
+        # Add empty slices to fully index the object
+        elif len(indices) < rank:
+            indices = indices + tuple([Slice(None, None)]*(rank-len(indices)))
+            self._indices = tuple(LiteralInteger(a) if isinstance(a, int) else a for a in indices)
+        else:
+            self._indices = tuple(LiteralInteger(a) if isinstance(a, int) else a for a in indices)
 
         # Calculate new shape
         new_shape = []
@@ -918,7 +921,6 @@ class IndexedElement(TypedAstNode):
                 new_shape.append(_shape)
         self._rank  = len(new_shape)
         self._shape = None if self._rank == 0 else tuple(new_shape)
-
         self._order = None if self.rank < 2 else base.order
 
         if self.rank == 0:
@@ -926,6 +928,8 @@ class IndexedElement(TypedAstNode):
         else:
             self._class_type = base.class_type
 
+        super().__init__()
+
     @property
     def base(self):
         """ The object which is indexed
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index e071b0f4..c332ab43 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -569,6 +569,7 @@ class CCodePrinter(CodePrinter):
             else:
                 args.append(a.value)
 
+        # Create new local variables to ensure there are no name collisions
         new_local_vars = [self.scope.get_temporary_variable(v) \
                             for v in func.local_vars]
 
@@ -762,7 +763,7 @@ class CCodePrinter(CodePrinter):
                         class_scope.rename_function(func, f"{classDef.name}__{func.name.lstrip('__')}")
                         funcs += f"{self.function_signature(func)};\n"
             classes += "};\n"
-        funcs += '\n'.join(f"{self.function_signature(f)};" for f in expr.module.funcs)
+        funcs += '\n'.join(f"{self.function_signature(f)};" for f in expr.module.funcs if not f.is_inline)
 
         global_variables = ''.join(['extern '+self._print(d) for d in expr.module.declarations if not d.variable.is_private])
 
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index a0c2e953..0b895065 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -22,7 +22,7 @@ from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
 from pyccel.ast.builtins import PythonTuple
 from pyccel.ast.builtins import PythonBool, PythonAbs
 
-from pyccel.ast.core import FunctionDef, InlineFunctionDef
+from pyccel.ast.core import FunctionDef
 from pyccel.ast.core import SeparatorComment, Comment
 from pyccel.ast.core import ConstructorCall
 from pyccel.ast.core import FunctionCallArgument
@@ -44,7 +44,7 @@ from pyccel.ast.internals import get_final_precision
 
 from pyccel.ast.itertoolsext import Product
 
-from pyccel.ast.literals  import LiteralInteger, LiteralFloat, Literal
+from pyccel.ast.literals  import LiteralInteger, LiteralFloat, Literal, LiteralEllipsis
 from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralString
 from pyccel.ast.literals  import Nil
 
@@ -408,10 +408,12 @@ class FCodePrinter(CodePrinter):
         self._additional_imports.update(func.imports)
         if func.global_vars or func.global_funcs:
             mod = func.get_direct_user_nodes(lambda x: isinstance(x, Module))[0]
-            self._additional_imports.add(Import(mod.name, [AsName(v, v.name) \
-                for v in (*func.global_vars, *func.global_funcs)]))
-            for v in (*func.global_vars, *func.global_funcs):
-                self.scope.insert_symbol(v.name)
+            current_mod = expr.get_user_nodes(Module, excluded_nodes=(FunctionCall,))[0]
+            if current_mod is not mod:
+                self._additional_imports.add(Import(mod.name, [AsName(v, v.name) \
+                          for v in (*func.global_vars, *func.global_funcs)]))
+                for v in (*func.global_vars, *func.global_funcs):
+                    self.scope.insert_symbol(v.name)
 
         self.set_scope(scope)
         return code
@@ -612,7 +614,7 @@ class FCodePrinter(CodePrinter):
         if len(targets) == 0:
             return 'use {}\n'.format(source)
 
-        targets = [t for t in targets if not isinstance(t.object, InlineFunctionDef)]
+        targets = [t for t in targets if not getattr(t.object, 'is_inline', False)]
         if len(targets) == 0:
             return ''
 
@@ -1800,6 +1802,9 @@ class FCodePrinter(CodePrinter):
 
     def _print_Interface(self, expr):
         # ... we don't print 'hidden' functions
+        if expr.functions[0].is_inline:
+            return ''
+
         name = self._print(expr.name)
         if all(isinstance(f, FunctionAddress) for f in expr.functions):
             funcs = expr.functions
@@ -2818,6 +2823,9 @@ class FCodePrinter(CodePrinter):
         base_code = self._print(base)
 
         inds = list(expr.indices)
+        if len(inds) == 1 and isinstance(inds[0], LiteralEllipsis):
+            inds = [Slice(None,None)]*expr.rank
+
         if expr.base.order == 'C':
             inds = inds[::-1]
         allow_negative_indexes = base.allows_negative_indexes
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 7fc07f7b..eb40c63d 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -26,6 +26,7 @@ from pyccel.codegen.printing.codeprinter import CodePrinter
 
 from pyccel.errors.errors import Errors
 from pyccel.errors.messages import PYCCEL_RESTRICTION_TODO
+from pyccel.parser.extend_tree import unparse
 
 errors = Errors()
 
@@ -305,7 +306,6 @@ class PythonCodePrinter(CodePrinter):
 
     def _print_Interface(self, expr):
         # TODO: Improve. See #885
-
         # Print each function in the interface
         func_def_code = []
         for func in expr.functions:
@@ -322,6 +322,10 @@ class PythonCodePrinter(CodePrinter):
         return func_def_code[0]
 
     def _print_FunctionDef(self, expr):
+        if expr.is_inline and not expr.is_semantic:
+            code = unparse(expr.python_ast) + '\n'
+            return code
+
         self.set_scope(expr.scope)
         name       = self._print(expr.name)
         imports    = ''.join(self._print(i) for i in expr.imports)
@@ -870,6 +874,9 @@ class PythonCodePrinter(CodePrinter):
                 stop  = stop,
                 step  = step)
 
+    def _print_LiteralEllipsis(self, expr):
+        return '...'
+
     def _print_SetMethod(self, expr):
         set_var = self._print(expr.set_variable)
         name = expr.name
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 18b64c88..357258be 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -1037,6 +1037,7 @@ class CToPythonWrapper(Wrapper):
 
         # Wrap functions
         funcs_to_wrap = [f for f in expr.funcs if f not in (expr.init_func, expr.free_func)]
+        funcs_to_wrap = [f for f in funcs_to_wrap if not f.is_inline]
 
         # Add any functions removed by the Fortran printer
         removed_functions = getattr(expr, 'removed_functions', None)
@@ -1046,7 +1047,7 @@ class CToPythonWrapper(Wrapper):
         funcs = [self._wrap(f) for f in funcs_to_wrap]
 
         # Wrap interfaces
-        interfaces = [self._wrap(i) for i in expr.interfaces]
+        interfaces = [self._wrap(i) for i in expr.interfaces if not i.is_inline]
 
         init_func = self._build_module_init_function(expr, imports)
 
diff --git a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
index 19e7eb9b..d9b9d2b2 100644
--- a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
+++ b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
@@ -172,7 +172,9 @@ class FortranToCWrapper(Wrapper):
         self.scope = mod_scope
 
         # Wrap contents
-        funcs_to_wrap = expr.funcs
+        # We only wrap the non inlined functions
+        funcs_to_wrap = [f for f in expr.funcs if f.is_semantic and not f.is_inline]
+
         funcs = [self._wrap(f) for f in funcs_to_wrap]
         if expr.init_func:
             init_func = funcs[next(i for i,f in enumerate(funcs_to_wrap) if f == expr.init_func)]
@@ -184,7 +186,7 @@ class FortranToCWrapper(Wrapper):
             free_func = None
         removed_functions = [f for f,w in zip(funcs_to_wrap, funcs) if isinstance(w, EmptyNode)]
         funcs = [f for f in funcs if not isinstance(f, EmptyNode)]
-        interfaces = [self._wrap(f) for f in expr.interfaces]
+        interfaces = [self._wrap(f) for f in expr.interfaces if not f.is_inline]
         classes = [self._wrap(f) for f in expr.classes]
         variables = [self._wrap(v) for v in expr.variables if not v.is_private]
         variable_getters = [v for v in variables if isinstance(v, BindCArrayVariable)]
diff --git a/pyccel/epyccel.py b/pyccel/epyccel.py
index fe849c1b..a3a3da28 100644
--- a/pyccel/epyccel.py
+++ b/pyccel/epyccel.py
@@ -9,24 +9,18 @@ import inspect
 import importlib
 import sys
 import os
-import string
-import random
+
 from filelock import FileLock, Timeout
 
 from types import ModuleType, FunctionType
 from importlib.machinery import ExtensionFileLoader
 
-from pyccel.codegen.pipeline import execute_pyccel
-from pyccel.errors.errors import ErrorsMode
-
-__all__ = ['random_string', 'get_source_function', 'epyccel_seq', 'epyccel']
+from pyccel.utilities.strings  import random_string
+from pyccel.codegen.pipeline   import execute_pyccel
+from pyccel.errors.errors      import ErrorsMode
 
-#==============================================================================
-random_selector = random.SystemRandom()
+__all__ = ['get_source_function', 'epyccel_seq', 'epyccel']
 
-def random_string( n ):
-    chars    = string.ascii_lowercase + string.digits
-    return ''.join( random_selector.choice( chars ) for _ in range(n) )
 
 #==============================================================================
 def get_source_function(func):
diff --git a/pyccel/parser/extend_tree.py b/pyccel/parser/extend_tree.py
index 515db1e2..c1bb9ab9 100644
--- a/pyccel/parser/extend_tree.py
+++ b/pyccel/parser/extend_tree.py
@@ -3,10 +3,13 @@
 # go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
 #------------------------------------------------------------------------------------------#
 
-"""Extended AST with CommentLine nodes
-======================================
+"""Extended AST with CommentLine and CommentMultiLine nodes
+===========================================================
 
 """
+from io import StringIO
+
+import astunparse
 
 from numpy import array, logical_and, where
 from ast   import AST, If as IfNode, parse
@@ -30,6 +33,56 @@ class CommentLine(AST):
 class CommentMultiLine(CommentLine):
     """"New AST node representing a multi-line comment"""
 
+# ast.unparse only available in python>3.8
+class Unparser(astunparse.Unparser):
+    """
+    Class which extends `astunparse.Unparser` to handle CommentLine nodes.
+
+    Class which extends `astunparse.Unparser` to handle CommentLine nodes.
+
+    Parameters
+    ----------
+    tree : ast.AST
+       The AST to be unparsed.
+
+    file : StringIO
+       The in-memory stream for unparsed code.
+    """
+    def __init__(self, tree, file):
+        super().__init__(tree, file=file)
+
+    def _CommentLine(self, node):
+        """
+        Method to unparse the CommentLine node.
+
+        Method to unparse the CommentLine node.
+
+        Parameters
+        ----------
+        node : CommentLine
+            The AST node that represents a comment.
+        """
+        self.write('\n'+' '*4*self._indent+node.s)
+
+def unparse(tree):
+    """
+    Unparse the AST.
+
+    Unparse the AST and return the code.
+
+    Parameters
+    ----------
+    tree : ast.AST
+       The AST to be unparsed.
+
+    Returns
+    -------
+    str
+       The code of the unparsed tree.
+    """
+    v = StringIO()
+    Unparser(tree, v)
+    return v.getvalue()
 
 def get_comments(code):
     lines = code.split("\n")
diff --git a/pyccel/parser/scope.py b/pyccel/parser/scope.py
index 1ce5037b..e14c83b0 100644
--- a/pyccel/parser/scope.py
+++ b/pyccel/parser/scope.py
@@ -33,6 +33,9 @@ class Scope(object):
 
     Parameters
     ----------
+    name : str, optional
+        The name of the scope. The value needs to be provided when it is not a loop.
+
     decorators : dict, default: ()
         A dictionary of any decorators which operate on objects in this scope.
 
@@ -53,7 +56,7 @@ class Scope(object):
     """
     allow_loop_scoping = False
     name_clash_checker = PythonNameClashChecker()
-    __slots__ = ('_imports','_locals','_parent_scope','_sons_scopes',
+    __slots__ = ('_name', '_imports','_locals','_parent_scope','_sons_scopes',
             '_is_loop','_loops','_temporary_variables', '_used_symbols',
             '_dummy_counter','_original_symbol', '_dotted_symbols')
 
@@ -62,10 +65,11 @@ class Scope(object):
             'macros','templates','headers','decorators',
             'cls_constructs')
 
-    def __init__(self, *, decorators = (), is_loop = False,
+    def __init__(self, *, name=None, decorators = (), is_loop = False,
                     parent_scope = None, used_symbols = None,
                     original_symbols = None):
 
+        self._name    = name
         self._imports = {k:{} for k in self.categories}
 
         self._locals  = {k:{} for k in self.categories}
@@ -113,26 +117,33 @@ class Scope(object):
         ----------
         name : str
             Name of the new scope, used as a key to retrieve the new scope.
-
-        kwargs : dict
+        **kwargs : dict
             Keyword arguments passed to __init__() for object initialization.
 
         Returns
         -------
-        child : Scope
+        Scope
             New child scope, which has the current object as parent.
-
         """
         ps = kwargs.pop('parent_scope', self)
         if ps is not self:
             raise ValueError("A child of {} cannot have a parent {}".format(self, ps))
 
-        child = Scope(**kwargs, parent_scope = self)
+        child = Scope(name=name, **kwargs, parent_scope = self)
 
         self.add_son(name, child)
 
         return child
 
+    @property
+    def name(self):
+        """
+        The name of the scope.
+
+        The name of the scope.
+        """
+        return self._name
+
     @property
     def imports(self):
         """ A dictionary of objects imported in this scope
@@ -488,6 +499,23 @@ class Scope(object):
                 self._used_symbols[symbol] = collisionless_symbol
                 self._original_symbol[collisionless_symbol] = symbol
 
+    def remove_symbol(self, symbol):
+        """
+        Remove symbol from the scope.
+
+        Remove symbol from the scope.
+
+        Parameters
+        ----------
+        symbol : PyccelSymbol
+            The symbol to be removed from the scope.
+        """
+
+        if symbol in self._used_symbols:
+            collisionless_symbol = self._used_symbols.pop(symbol)
+            self._original_symbol.pop(collisionless_symbol)
+
+
     def insert_symbolic_alias(self, symbol, alias):
         """
         Add a new symbolic alias to the scope.
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 89a38e8a..b7469788 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -20,8 +20,8 @@ from sympy import Integer as sp_Integer
 from sympy import ceiling
 
 #==============================================================================
-
-from pyccel.ast.basic import PyccelAstNode, TypedAstNode, ScopedAstNode
+from pyccel.utilities.strings import random_string
+from pyccel.ast.basic         import PyccelAstNode, TypedAstNode, ScopedAstNode
 
 from pyccel.ast.builtins import PythonPrint, PythonTupleFunction
 from pyccel.ast.builtins import PythonComplex
@@ -905,7 +905,7 @@ class SemanticParser(BasicParser):
                 severity='error')
 
         for arg in var[indices].indices:
-            if not isinstance(arg, Slice) and not \
+            if not isinstance(arg, (Slice, LiteralEllipsis)) and not \
                 (hasattr(arg, 'dtype') and isinstance(arg.dtype, NativeInteger)):
                 errors.report(INVALID_INDICES, symbol=var[indices],
                 bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
@@ -1012,6 +1012,9 @@ class SemanticParser(BasicParser):
         """
         args  = []
         for arg in arguments:
+            a = self._visit(arg.value)
+            if isinstance(a, FunctionDef) and not isinstance(a, PyccelFunctionDef) and not a.is_semantic:
+                self._annotate_the_called_function_def(a)
             a = self._visit(arg)
             if isinstance(a.value, StarredArguments):
                 args.extend([FunctionCallArgument(av) for av in a.value.args_var])
@@ -1039,20 +1042,31 @@ class SemanticParser(BasicParser):
             descr += f'[{dims}]'
         return descr
 
-    def _check_argument_compatibility(self, input_args, func_args, expr, elemental):
+    def _check_argument_compatibility(self, input_args, func_args, func, elemental, raise_error=True, error_type='error'):
         """
-        Check that the provided arguments match the expected types
+        Check that the provided arguments match the expected types.
+
+        Check that the provided arguments match the expected types.
 
         Parameters
         ----------
         input_args : list
-                     The arguments provided to the function
-        func_args  : list
-                     The arguments expected by the function
-        expr       : TypedAstNode
-                     The expression where this call is found (used for error output)
-        elemental  : bool
-                     Indicates if the function is elemental
+           The arguments provided to the function.
+        func_args : list
+           The arguments expected by the function.
+        func : FunctionDef
+           The called function (used for error output).
+        elemental : bool
+           Indicates if the function is elemental.
+        raise_error : bool, default : True
+           Raise the error if the arguments are incompatible.
+        error_type : str, default : error
+           The error type if errors are raised from the function.
+
+        Returns
+        -------
+        bool
+            Return True if the arguments are compatible, False otherwise.
         """
         if elemental:
             def incompatible(i_arg, f_arg):
@@ -1064,6 +1078,7 @@ class SemanticParser(BasicParser):
                         get_final_precision(i_arg) != get_final_precision(f_arg) or
                         i_arg.rank != f_arg.rank)
 
+        err_msgs = []
         # Compare each set of arguments
         for idx, (i_arg, f_arg) in enumerate(zip(input_args, func_args)):
             i_arg = i_arg.value
@@ -1073,19 +1088,24 @@ class SemanticParser(BasicParser):
                     or isinstance(f_arg, FunctionAddress)
                     or f_arg.dtype is NativeGeneric()):
                 continue
+
             # Check for compatibility
             if incompatible(i_arg, f_arg):
                 expected  = self.get_type_description(f_arg, not elemental)
                 type_name = self.get_type_description(i_arg, not elemental)
                 received  = f'{i_arg} ({type_name})'
+                err_msgs += [INCOMPATIBLE_ARGUMENT.format(idx+1, received, func, expected)]
 
-                errors.report(INCOMPATIBLE_ARGUMENT.format(idx+1, received, expr.func_name, expected),
-                        symbol = expr,
-                        severity='error')
             if f_arg.rank > 1 and i_arg.order != f_arg.order:
-                errors.report(INCOMPATIBLE_ORDERING.format(idx=idx+1, arg=i_arg, func=expr.func_name, order=f_arg.order),
-                        symbol = expr,
-                        severity='error')
+                err_msgs += [INCOMPATIBLE_ORDERING.format(idx=idx+1, arg=i_arg, func=func, order=f_arg.order)]
+
+        if err_msgs:
+            if raise_error:
+                bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset)
+                errors.report('\n\n'.join(err_msgs), symbol = func, bounding_box=bounding_box, severity=error_type)
+            else:
+                return False
+        return True
 
     def _handle_function(self, expr, func, args, is_method = False):
         """
@@ -1142,9 +1162,7 @@ class SemanticParser(BasicParser):
 
             parent_assign = expr.get_direct_user_nodes(lambda x: isinstance(x, Assign) and not isinstance(x, AugAssign))
 
-            func_args = func.arguments if isinstance(func, FunctionDef) else func.functions[0].arguments
             func_results = func.results if isinstance(func, FunctionDef) else func.functions[0].results
-
             if not parent_assign and len(func_results) == 1 and func_results[0].var.rank > 0:
                 tmp_var = PyccelSymbol(self.scope.get_new_name())
                 assign = Assign(tmp_var, expr)
@@ -1152,27 +1170,13 @@ class SemanticParser(BasicParser):
                 self._additional_exprs[-1].append(self._visit(assign))
                 return self._visit(tmp_var)
 
+            func_args = func.arguments if isinstance(func,FunctionDef) else func.functions[0].arguments
             if len(args) > len(func_args):
                 errors.report("Too many arguments passed in function call",
                         symbol = expr,
                         severity='fatal')
-            # Sort arguments to match the order in the function definition
-            input_args = [a for a in args if a.keyword is None]
-            nargs = len(input_args)
-            for ka in func_args[nargs:]:
-                key = ka.name
-                relevant_args = [a for a in args[nargs:] if a.keyword == key]
-                n_relevant_args = len(relevant_args)
-                assert n_relevant_args <= 1
-                if n_relevant_args == 0 and ka.has_default:
-                    input_args.append(ka.default_call_arg)
-                elif n_relevant_args == 1:
-                    input_args.append(relevant_args[0])
-
-            args = input_args
 
             new_expr = FunctionCall(func, args, self._current_function)
-
             for a, f_a in zip(new_expr.args, func_args):
                 if f_a.persistent_target:
                     assert is_method
@@ -1190,9 +1194,106 @@ class SemanticParser(BasicParser):
                         severity='error')
             elif isinstance(func, FunctionDef):
                 self._check_argument_compatibility(args, func_args,
-                            expr, func.is_elemental)
+                            func, func.is_elemental)
+
             return new_expr
 
+    def _sort_function_call_args(self, func_args, args):
+        """
+        Sort and add the missing call arguments to match the arguments in the function definition.
+
+        We sort the call arguments by dividing them into two chunks, positional arguments and keyword arguments.
+        We provide the default value of the keyword argument if the corresponding call argument is not present.
+
+        Parameters
+        ----------
+        func_args : list[FunctionDefArgument]
+          The arguments of the function definition.
+        args : list[FunctionCallArgument]
+          The arguments of the function call.
+
+        Returns
+        -------
+        list[FunctionCallArgument]
+            The sorted and complete call arguments.
+        """
+        input_args = [a for a in args if a.keyword is None]
+        nargs = len(input_args)
+        for ka in func_args[nargs:]:
+            key = ka.name
+            relevant_args = [a for a in args[nargs:] if a.keyword == key]
+            n_relevant_args = len(relevant_args)
+            assert n_relevant_args <= 1
+            if n_relevant_args == 0 and ka.has_default:
+                input_args.append(ka.default_call_arg)
+            elif n_relevant_args == 1:
+                input_args.append(relevant_args[0])
+
+        return input_args
+
+    def _annotate_the_called_function_def(self, old_func, function_call_args=None):
+        """
+        Annotate the called FunctionDef.
+
+        Annotate the called FunctionDef.
+
+        Parameters
+        ----------
+        old_func : FunctionDef|Interface
+           The function that needs to be annotated.
+
+        function_call_args : list[FunctionCallArgument], optional
+           The list of the call arguments.
+
+        Returns
+        -------
+        func: FunctionDef|Interface
+            The new annotated function.
+        """
+        # The function call might be in a completely different scope from the FunctionDef
+        # Store the current scope and go to the parent scope of the FunctionDef
+        old_scope            = self._scope
+        old_current_function = self._current_function
+        names = []
+        sc = old_func.scope if isinstance(old_func, FunctionDef) else old_func.syntactic_node.scope
+        while sc.parent_scope is not None:
+            sc = sc.parent_scope
+            if not sc.name is None:
+                names.append(sc.name)
+        names.reverse()
+        if names:
+            self._current_function = DottedName(*names) if len(names)>1 else names[0]
+        else:
+            self._current_function = None
+
+        while names:
+            sc = sc.sons_scopes[names[0]]
+            names = names[1:]
+
+        # Set the Scope to the FunctionDef's parent Scope and annotate the old_func
+        self._scope = sc
+        self._visit_FunctionDef(old_func, annotate=True, function_call_args=function_call_args)
+        # Retreive the annotated function
+        func = self.scope.find(old_func.name, 'functions')
+        # Add the Module of the imported function to the new function
+        if old_func.is_imported:
+            mod = old_func.get_direct_user_nodes(lambda x: isinstance(x, Module))[0]
+            func.set_current_user_node(mod)
+
+        # Go back to the original Scope
+        self._scope = old_scope
+        self._current_function = old_current_function
+        # Remove the old_func from the imports dict and Assign the new annotated one
+        if old_func.is_imported:
+            scope = self.scope
+            while old_func.name not in scope.imports['functions']:
+                scope = scope.parent_scope
+            assert old_func is scope.imports['functions'].get(old_func.name)
+            func = func.clone(old_func.name, is_imported=True)
+            func.set_current_user_node(mod)
+            scope.imports['functions'][old_func.name] = func
+        return func
+
     def _create_variable(self, name, dtype, rhs, d_lhs, arr_in_multirets=False):
         """
         Create a new variable.
@@ -2026,6 +2127,12 @@ class SemanticParser(BasicParser):
             else:
                 init_func_body.append(b)
 
+        for f in self.scope.functions.copy():
+            f = self.scope.functions[f]
+            if not f.is_semantic and not isinstance(f, InlineFunctionDef):
+                assert isinstance(f, FunctionDef)
+                self._visit_FunctionDef(f, annotate=True)
+
         variables = self.get_variables(self.scope)
         init_func = None
         free_func = None
@@ -2173,6 +2280,7 @@ class SemanticParser(BasicParser):
             if init_func:
                 import_init  = FunctionCall(init_func,[],[])
                 program_body = [import_init, *program_body]
+
             if free_func:
                 import_free  = FunctionCall(free_func,[],[])
                 program_body = [*program_body, import_free]
@@ -2184,7 +2292,6 @@ class SemanticParser(BasicParser):
                             scope=self._program_namespace)
 
             mod.program = program
-
         return mod
 
     def _visit_PythonTuple(self, expr):
@@ -2940,14 +3047,35 @@ class SemanticParser(BasicParser):
                 return getattr(self, annotation_method)(expr)
 
         args = self._handle_function_args(expr.args)
+
         # Correct keyword names if scope is available
         # The scope is only available if the function body has been parsed
         # (i.e. not for headers or builtin functions)
-        if isinstance(func, FunctionDef) and func.scope:
+        if (isinstance(func, FunctionDef) and func.scope) or isinstance(func, Interface):
+            scope = func.scope if isinstance(func, FunctionDef) else func.functions[0].scope
             args = [a if a.keyword is None else \
-                    FunctionCallArgument(a.value, func.scope.get_expected_name(a.keyword)) \
+                    FunctionCallArgument(a.value, scope.get_expected_name(a.keyword)) \
                     for a in args]
-
+            func_args = func.arguments if isinstance(func,FunctionDef) else func.functions[0].arguments
+            if not func.is_semantic:
+                # Correct func_args keyword names
+                func_args = [FunctionDefArgument(AnnotatedPyccelSymbol(scope.get_expected_name(a.var.name), a.annotation),
+                            annotation=a.annotation, value=a.value, kwonly=a.is_kwonly, bound_argument=a.bound_argument)
+                            for a in func_args]
+            args      = self._sort_function_call_args(func_args, args)
+            is_inline = func.is_inline if isinstance(func, FunctionDef) else func.functions[0].is_inline
+            if not func.is_semantic:
+                if not is_inline:
+                    func = self._annotate_the_called_function_def(func)
+                else:
+                    func = self._annotate_the_called_function_def(func, function_call_args=args)
+            elif is_inline and isinstance(func, Interface):
+                is_compatible = False
+                for f in func.functions:
+                    fl = self._check_argument_compatibility(args, f.arguments, func, f.is_elemental, raise_error=False)
+                    is_compatible |= fl
+                if not is_compatible:
+                    func = self._annotate_the_called_function_def(func, function_call_args=args)
 
         if name == 'lambdify':
             args = self.scope.find(str(expr.args[0]), 'symbolic_functions')
@@ -2995,7 +3123,7 @@ class SemanticParser(BasicParser):
             self._additional_exprs[-1].extend(new_expression)
             args = (FunctionCallArgument(cls_variable), *args)
             self._check_argument_compatibility(args, method.arguments,
-                            expr, method.is_elemental)
+                            method, method.is_elemental)
 
             new_expr = ConstructorCall(method, args, cls_variable)
 
@@ -3021,8 +3149,7 @@ class SemanticParser(BasicParser):
                 func = macro.master.funcdef
                 name = _get_name(func.name)
                 args = macro.apply(args)
-            else:
-                func = self.scope.find(name, 'functions')
+
             if func is None:
                 return errors.report(UNDEFINED_FUNCTION, symbol=name,
                         bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
@@ -3710,6 +3837,7 @@ class SemanticParser(BasicParser):
             self.change_to_program_scope()
         else:
             cond = self._visit(expr.condition)
+
         body = self._visit(expr.body)
         if prog_check:
             # Calling the Garbage collecting,
@@ -3822,17 +3950,55 @@ class SemanticParser(BasicParser):
             expr  = Return(results)
         return expr
 
-    def _visit_FunctionDef(self, expr):
-        name            = self.scope.get_expected_name(expr.name)
-        decorators      = expr.decorators
-        funcs           = []
-        sub_funcs       = []
-        func_interfaces = []
-        is_pure         = expr.is_pure
-        is_elemental    = expr.is_elemental
-        is_private      = expr.is_private
-        is_inline       = expr.is_inline
-        docstring      = self._visit(expr.docstring) if expr.docstring else expr.docstring
+    def _visit_FunctionDef(self, expr, annotate=False, function_call_args=None):
+        """
+        Annotate the FunctionDef if necessary.
+
+        The FunctionDef is only annotated if the flag annotate is set to True.
+        In the case of an inlined function, we always annotate the function partially,
+        depending on the function call if it is an interface, otherwise we annotate it
+        if the function_call argument are compatible with the FunctionDef arguments.
+        In the case of non inlined function, we only pass through this method
+        twice, the first time we do nothing and the second time we annotate all of functions.
+
+        Parameter
+        ---------
+        expr : FunctionDef|Interface
+           The node that needs to be annotated.
+           If we provide an Interface, this means that the function has been annotated partially,
+           and we need to continue annotating the needed ones.
+
+        annotate : bool, default: False
+           Annotate expr if the flag is set to True.
+
+        function_call_args : list[FunctionCallArgument], optional
+            The list of call arguments, needed only in the case of an inlined function.
+        """
+        if not annotate:
+            self.insert_function(expr)
+            return EmptyNode()
+
+        existing_semantic_funcs = []
+        if not expr.is_semantic:
+            self.scope.functions.pop(expr.name, None)
+        elif isinstance(expr, Interface):
+            existing_semantic_funcs = [*expr.functions]
+            expr                    = expr.syntactic_node
+
+        name               = self.scope.get_expected_name(expr.name)
+        decorators         = expr.decorators
+        new_semantic_funcs = []
+        sub_funcs          = []
+        func_interfaces    = []
+        docstring          = self._visit(expr.docstring) if expr.docstring else expr.docstring
+        is_pure            = expr.is_pure
+        is_elemental       = expr.is_elemental
+        is_private         = expr.is_private
+        is_inline          = expr.is_inline
+
+        if function_call_args is not None:
+            assert is_inline
+            found_func = False
 
         current_class = expr.get_direct_user_nodes(lambda u: isinstance(u, ClassDef))
         cls_name = current_class[0].name if current_class else None
@@ -3851,15 +4017,40 @@ class SemanticParser(BasicParser):
         for t,v in templates.items():
             templates[t] = UnionTypeAnnotation(*[self._visit(vi) for vi in v])
 
+        def unpack(ann):
+            if isinstance(ann, UnionTypeAnnotation):
+                return ann.type_list
+            else:
+                return [ann]
+
         # Filter out unused templates
-        templatable_args = [a.annotation for a in expr.arguments if isinstance(a.annotation, (SyntacticTypeAnnotation, UnionTypeAnnotation))]
-        arg_annotations = [annot for a in templatable_args for annot in (a.type_list \
-                                        if isinstance(a, UnionTypeAnnotation) else [a]) \
-                                        if isinstance(annot, SyntacticTypeAnnotation)]
+        templatable_args = [unpack(a.annotation) for a in expr.arguments if isinstance(a.annotation, (SyntacticTypeAnnotation, UnionTypeAnnotation))]
+        arg_annotations = [annot for a in templatable_args for annot in a if isinstance(annot, SyntacticTypeAnnotation)]
         type_names = [a.dtype for a in arg_annotations]
         used_type_names = set(d.base if isinstance(d, IndexedElement) else d for d in type_names)
         templates = {t: v for t,v in templates.items() if t in used_type_names}
 
+        # Create new temparary templates for the arguments with a Union data type.
+        tmp_templates = {}
+        new_expr_args = []
+        for a in expr.arguments:
+            if isinstance(a.annotation, UnionTypeAnnotation):
+                annotation = [aa for a in a.annotation for aa in unpack(a)]
+            else:
+                annotation = [a.annotation]
+            if len(annotation)>1:
+                tmp_template_name = a.name + '_' + random_string(12)
+                tmp_template_name = self.scope.get_new_name(tmp_template_name)
+                tmp_templates[tmp_template_name] = UnionTypeAnnotation(*[self._visit(vi) for vi in annotation])
+                dtype_symb = PyccelSymbol(tmp_template_name, is_temp=True)
+                dtype_symb = SyntacticTypeAnnotation(dtype_symb)
+                var_clone = AnnotatedPyccelSymbol(a.var.name, annotation=dtype_symb, is_temp=a.var.name.is_temp)
+                new_expr_args.append(FunctionDefArgument(var_clone, bound_argument=a.bound_argument,
+                                        value=a.value, kwonly=a.is_kwonly, annotation=dtype_symb))
+            else:
+                new_expr_args.append(a)
+
+        templates.update(tmp_templates)
         template_combinations = list(product(*[v.type_list for v in templates.values()]))
         template_names = list(templates.keys())
         n_templates = len(template_combinations)
@@ -3867,197 +4058,224 @@ class SemanticParser(BasicParser):
         # this for the case of a function without arguments => no headers
         interface_name = name
         interface_counter = 0
-
+        is_interface = n_templates > 1
+        annotated_args = [] # collect annotated arguments to check for argument incompatibility errors
         for tmpl_idx in range(n_templates):
-            # Change to syntactic FunctionDef scope to ensure get_expected_name is available
+            if function_call_args is not None and found_func:
+                break
+
+            if is_interface:
+                name, _ = self.scope.get_new_incremented_symbol(interface_name, tmpl_idx)
+
             scope = self.create_new_function_scope(name, decorators = decorators,
                     used_symbols = expr.scope.local_used_symbols.copy(),
                     original_symbols = expr.scope.python_names.copy())
+
             for n, v in zip(template_names, template_combinations[tmpl_idx]):
                 self.scope.insert_symbolic_alias(n, v)
             self.scope.decorators.update(decorators)
-            arguments = [self._visit(a) for a in expr.arguments]
+
+            # Here _visit_AnnotatedPyccelSymbol always give us an list of size 1
+            # so we flatten the arguments
+            arguments = [i for a in new_expr_args for i in self._visit(a)]
+            assert len(arguments) == len(expr.arguments)
+            arg_dict  = {a.name:a.var for a in arguments}
+            annotated_args.append(arguments)
             for n in template_names:
                 self.scope.symbolic_alias.pop(n)
-            self.exit_function_scope()
-
-            n_interface_funcs = prod(len(a) for a in arguments)
-            argument_vars = list(product(*arguments))
-
-            is_interface = n_templates > 1 or n_interface_funcs > 1
-
-            for i in range(n_interface_funcs):
-                arguments      = argument_vars[i]
-                arg_dict = {a.name:a.var for a in arguments}
-
-                if is_interface:
-                    name, interface_counter = self.scope.get_new_incremented_symbol(interface_name, interface_counter)
-                scope = self.create_new_function_scope(name, decorators = decorators,
-                        used_symbols = expr.scope.local_used_symbols.copy(),
-                        original_symbols = expr.scope.python_names.copy())
-
-                if len(arguments)>0 and arguments[0].bound_argument:
-                    if arguments[0].var.cls_base.name != cls_name:
-                        errors.report('Class method self argument does not have the expected type',
-                                severity='error', symbol=arguments[0])
-                    for s in expr.scope.dotted_symbols:
-                        base = s.name[0]
-                        if base in arg_dict:
-                            cls_base = arg_dict[base].cls_base
-                            cls_base.scope.insert_symbol(DottedName(*s.name[1:]))
-
-                for a in arguments:
-                    a_var = a.var
-                    if isinstance(a_var, FunctionAddress):
-                        self.insert_function(a_var)
-                    else:
-                        self.scope.insert_variable(a_var, expr.scope.get_python_name(a.name))
-
-                results = expr.results
-                if results and results[0].annotation:
-                    results = [self._visit(r) for r in expr.results]
-
-                # insert the FunctionDef into the scope
-                # to handle the case of a recursive function
-                # TODO improve in the case of an interface
-                recursive_func_obj = FunctionDef(name, arguments, results, [])
-                self.insert_function(recursive_func_obj)
-
-                # Create a new list that store local variables for each FunctionDef to handle nested functions
-                self._allocs.append(set())
-                self._pointer_targets.append({})
-
-                # we annotate the body
-                body = self._visit(expr.body)
-
-                # Calling the Garbage collecting,
-                # it will add the necessary Deallocate nodes
-                # to the body of the function
-                body.insert2body(*self._garbage_collector(body))
-                self._check_pointer_targets(results)
 
-                results = [self._visit(a) for a in results]
-
-                # Determine local and global variables
-                global_vars = list(self.get_variables(self.scope.parent_scope))
-                global_vars = [g for g in global_vars if body.is_user_of(g)]
-
-                # get the imports
-                imports   = self.scope.imports['imports'].values()
-                # Prefer dict to set to preserve order
-                imports   = list({imp:None for imp in imports}.keys())
+            if function_call_args is not None:
+                is_compatible = self._check_argument_compatibility(function_call_args, arguments, expr, is_elemental, raise_error=False)
+                if not is_compatible:
+                    self.exit_function_scope()
+                    # remove the new created scope and the function name
+                    self.scope.sons_scopes.pop(name)
+                    if is_interface:
+                        self.scope.remove_symbol(name)
+                    continue
+                #In the case of an Interface we set found_func to True so that we don't continue
+                #searching for the other functions
+                found_func = True
+
+            for a in arguments:
+                a_var = a.var
+                if isinstance(a_var, FunctionAddress):
+                    self.insert_function(a_var)
+                else:
+                    self.scope.insert_variable(a_var, expr.scope.get_python_name(a.name))
+
+            if arguments and arguments[0].bound_argument:
+                if arguments[0].var.cls_base.name != cls_name:
+                    errors.report('Class method self argument does not have the expected type',
+                            severity='error', symbol=arguments[0])
+                for s in expr.scope.dotted_symbols:
+                    base = s.name[0]
+                    if base in arg_dict:
+                        cls_base = arg_dict[base].cls_base
+                        cls_base.scope.insert_symbol(DottedName(*s.name[1:]))
+
+            results = expr.results
+            if results and results[0].annotation:
+                results = [self._visit(r) for r in expr.results]
+
+            # insert the FunctionDef into the scope
+            # to handle the case of a recursive function
+            # TODO improve in the case of an interface
+            recursive_func_obj = FunctionDef(name, arguments, results, [])
+            self.insert_function(recursive_func_obj)
+
+            # Create a new list that store local variables for each FunctionDef to handle nested functions
+            self._allocs.append(set())
+            self._pointer_targets.append({})
+
+            # we annotate the body
+            body = self._visit(expr.body)
+
+            # Annotate the remaining functions
+            sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and\
+                        not isinstance(i, (InlineFunctionDef, FunctionAddress)) and \
+                        not i.is_semantic]
+            for i in sub_funcs:
+                self._visit_FunctionDef(i, annotate=True)
 
-                # remove the FunctionDef from the function scope
-                # TODO improve func_ is None in the case of an interface
-                func_     = self.scope.functions.pop(name, None)
-                is_recursive = False
-                # check if the function is recursive if it was called on the same scope
-                if func_ and func_.is_recursive:
-                    is_recursive = True
+            # Calling the Garbage collecting,
+            # it will add the necessary Deallocate nodes
+            # to the body of the function
+            body.insert2body(*self._garbage_collector(body))
+            self._check_pointer_targets(results)
+
+            results = [self._visit(a) for a in results]
+
+            # Determine local and global variables
+            global_vars = list(self.get_variables(self.scope.parent_scope))
+            global_vars = [g for g in global_vars if body.is_user_of(g)]
+
+            # get the imports
+            imports   = self.scope.imports['imports'].values()
+            # Prefer dict to set to preserve order
+            imports   = list({imp:None for imp in imports}.keys())
+
+            # remove the FunctionDef from the function scope
+            func_     = self.scope.functions.pop(name)
+            is_recursive = False
+            # check if the function is recursive if it was called on the same scope
+            if func_.is_recursive and not is_inline:
+                is_recursive = True
+            elif func_.is_recursive and is_inline:
+                errors.report("Pyccel does not support an inlined recursive function", symbol=expr,
+                        severity='fatal')
 
-                sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
+            sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
 
-                func_args = [i for i in self.scope.functions.values() if isinstance(i, FunctionAddress)]
-                if func_args:
-                    func_interfaces.append(Interface('', func_args, is_argument = True))
+            func_args = [i for i in self.scope.functions.values() if isinstance(i, FunctionAddress)]
+            if func_args:
+                func_interfaces.append(Interface('', func_args, is_argument = True))
 
-                namespace_imports = self.scope.imports
-                self.exit_function_scope()
+            namespace_imports = self.scope.imports
+            self.exit_function_scope()
 
-                results_names = [i.var.name for i in results]
+            results_names = [i.var.name for i in results]
+
+            # Find all nodes which can modify variables
+            assigns = body.get_attribute_nodes(Assign, excluded_nodes = (FunctionCall,))
+            calls   = body.get_attribute_nodes(FunctionCall)
+
+            # Collect the modified objects
+            lhs_assigns   = [a.lhs for a in assigns]
+            modified_args = [call_arg.value for f in calls
+                                for call_arg, func_arg in zip(f.args, f.funcdef.arguments) if func_arg.inout]
+            # Collect modified variables
+            all_assigned = [v for a in (lhs_assigns + modified_args) for v in
+                            (a.get_attribute_nodes(Variable) if not isinstance(a, Variable) else [a])]
+
+            # ... computing inout arguments
+            for a in arguments:
+                if a.name not in chain(results_names, ['self']) and a.var not in all_assigned:
+                    a.make_const()
+            # ...
+            # Raise an error if one of the return arguments is an alias.
+            pointer_targets = self._pointer_targets.pop()
+            result_pointer_map = {}
+            for r in results:
+                t = pointer_targets.get(r.var, ())
+                if r.var.is_alias:
+                    persistent_targets = []
+                    for target, _ in t:
+                        target_argument_index = next((i for i,a in enumerate(arguments) if a.var == target), -1)
+                        if target_argument_index != -1:
+                            persistent_targets.append(target_argument_index)
+                    if not persistent_targets:
+                        errors.report(UNSUPPORTED_POINTER_RETURN_VALUE,
+                            symbol=r, severity='error',
+                            bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset))
+                    else:
+                        result_pointer_map[r] = persistent_targets
+
+            optional_inits = []
+            for a in arguments:
+                var = self._optional_params.pop(a.var, None)
+                if var:
+                    optional_inits.append(If(IfSection(PyccelIsNot(a.var, Nil()),
+                                                       [Assign(var, a.var)])))
+            body.insert2body(*optional_inits, back=False)
+
+            func_kwargs = {
+                    'global_vars':global_vars,
+                    'is_pure':is_pure,
+                    'is_elemental':is_elemental,
+                    'is_private':is_private,
+                    'imports':imports,
+                    'decorators':decorators,
+                    'is_recursive':is_recursive,
+                    'functions': sub_funcs,
+                    'interfaces': func_interfaces,
+                    'result_pointer_map': result_pointer_map,
+                    'docstring': docstring,
+                    'scope': scope,
+            }
+            if is_inline:
+                func_kwargs['namespace_imports'] = namespace_imports
+                global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.scope.find(f.name, 'functions')]
+                func_kwargs['global_funcs'] = global_funcs
+                cls = InlineFunctionDef
+            else:
+                cls = FunctionDef
+            func = cls(name,
+                    arguments,
+                    results,
+                    body,
+                    **func_kwargs)
+            if not is_recursive:
+                recursive_func_obj.invalidate_node()
 
-                # Find all nodes which can modify variables
-                assigns = body.get_attribute_nodes(Assign, excluded_nodes = (FunctionCall,))
-                calls   = body.get_attribute_nodes(FunctionCall)
+            if cls_name:
+                # update the class methods
+                if not is_interface:
+                    bound_class.add_new_method(func)
 
-                # Collect the modified objects
-                lhs_assigns   = [a.lhs for a in assigns]
-                modified_args = [call_arg.value for f in calls
-                                    for call_arg, func_arg in zip(f.args, f.funcdef.arguments) if func_arg.inout]
-                # Collect modified variables
-                all_assigned = [v for a in (lhs_assigns + modified_args) for v in
-                                (a.get_attribute_nodes(Variable) if not isinstance(a, Variable) else [a])]
+            new_semantic_funcs += [func]
 
-                # ... computing inout arguments
-                for a in arguments:
-                    if a.name not in chain(results_names, ['self']) and a.var not in all_assigned:
-                        a.make_const()
-                # ...
+        if function_call_args is not None and len(new_semantic_funcs) == 0:
+            for args in annotated_args[:-1]:
+                #raise errors if we do not find any compatible function def
+                self._check_argument_compatibility(function_call_args, args, expr, is_elemental, error_type='error')
+            self._check_argument_compatibility(function_call_args, annotated_args[-1], expr, is_elemental, error_type='fatal')
 
-                # Raise an error if one of the return arguments is an alias.
-                pointer_targets = self._pointer_targets.pop()
-                result_pointer_map = {}
-                for r in results:
-                    t = pointer_targets.get(r.var, ())
-                    if r.var.is_alias:
-                        persistent_targets = []
-                        for target, _ in t:
-                            target_argument_index = next((i for i,a in enumerate(arguments) if a.var == target), -1)
-                            if target_argument_index != -1:
-                                persistent_targets.append(target_argument_index)
-                        if not persistent_targets:
-                            errors.report(UNSUPPORTED_POINTER_RETURN_VALUE,
-                                symbol=r, severity='error',
-                                bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset))
-                        else:
-                            result_pointer_map[r] = persistent_targets
-
-                optional_inits = []
-                for a in arguments:
-                    var = self._optional_params.pop(a.var, None)
-                    if var:
-                        optional_inits.append(If(IfSection(PyccelIsNot(a.var, Nil()),
-                                                           [Assign(var, a.var)])))
-                body.insert2body(*optional_inits, back=False)
-
-                func_kwargs = {
-                        'global_vars':global_vars,
-                        'is_pure':is_pure,
-                        'is_elemental':is_elemental,
-                        'is_private':is_private,
-                        'imports':imports,
-                        'decorators':decorators,
-                        'is_recursive':is_recursive,
-                        'functions': sub_funcs,
-                        'interfaces': func_interfaces,
-                        'result_pointer_map': result_pointer_map,
-                        'docstring': docstring,
-                        'scope': scope
-                        }
-                if is_inline:
-                    func_kwargs['namespace_imports'] = namespace_imports
-                    global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.scope.find(f.name, 'functions')]
-                    func_kwargs['global_funcs'] = global_funcs
-                    cls = InlineFunctionDef
-                else:
-                    cls = FunctionDef
-                func = cls(name,
-                        arguments,
-                        results,
-                        body,
-                        **func_kwargs)
-                if not is_recursive:
-                    recursive_func_obj.invalidate_node()
-
-                if cls_name:
-                    # update the class methods
-                    if not is_interface:
-                        bound_class.add_new_method(func)
-
-                funcs += [func]
-
-        if len(funcs) == 1:
-            funcs = funcs[0]
-            self.insert_function(funcs)
+        if existing_semantic_funcs:
+            new_semantic_funcs = existing_semantic_funcs + new_semantic_funcs
 
+        if len(new_semantic_funcs) == 1 and not is_interface:
+            new_semantic_funcs = new_semantic_funcs[0]
+            self.insert_function(new_semantic_funcs)
         else:
-            for f in funcs:
+            for f in new_semantic_funcs:
                 self.insert_function(f)
 
-            funcs = Interface(interface_name, funcs)
+            new_semantic_funcs = Interface(interface_name, new_semantic_funcs, syntactic_node=expr)
             if cls_name:
-                bound_class.add_new_interface(funcs)
-            self.insert_function(funcs)
+                bound_class.add_new_interface(new_semantic_funcs)
+            self.insert_function(new_semantic_funcs)
+
         return EmptyNode()
 
     def _visit_PythonPrint(self, expr):
@@ -4141,7 +4359,7 @@ class SemanticParser(BasicParser):
             m_name = method.name
             if m_name == '__init__':
                 if init_func is None:
-                    self._visit(method)
+                    self._visit_FunctionDef(method, annotate=True)
                     init_func = self.scope.functions.pop(m_name)
 
                 if isinstance(init_func, Interface):
@@ -4164,7 +4382,7 @@ class SemanticParser(BasicParser):
                    severity='error')
 
         for i in methods:
-            self._visit(i)
+            self._visit_FunctionDef(i, annotate=True)
 
         if not any(method.name == '__del__' for method in methods):
             argument = FunctionDefArgument(Variable(cls.name, 'self', cls_base = cls), bound_argument = True)
@@ -4337,10 +4555,14 @@ class SemanticParser(BasicParser):
                     for t,n in zip(targets.keys(),names):
                         if n in d_son:
                             e = d_son[n]
-                            if t == n:
-                                container[entry][t] = e
-                            else:
+                            if entry == 'functions':
+                                container[entry][t] = e.clone(t, is_imported=True)
+                                m = e.get_direct_user_nodes(lambda x: isinstance(x, Module))[0]
+                                container[entry][t].set_current_user_node(m)
+                            elif entry == 'variables':
                                 container[entry][t] = e.clone(t)
+                            else:
+                                container[entry][t] = e
                             targets[t] = e
                 if None in targets.values():
                     errors.report("Import target {} could not be found",
diff --git a/pyccel/parser/utilities.py b/pyccel/parser/utilities.py
index 9403e9b5..54ac65a1 100644
--- a/pyccel/parser/utilities.py
+++ b/pyccel/parser/utilities.py
@@ -5,14 +5,18 @@
 #------------------------------------------------------------------------------------------#
 
 """This file contains different utilities for the Parser."""
+import os
 
 from sympy import srepr
-import os
 
 from pyccel.ast.variable       import DottedName
 from pyccel.parser.extend_tree import CommentLine
 from pyccel.ast.internals      import PyccelSymbol
 
+__all__ = ('pyccel_external_lib', 'read_file', 'is_valid_filename_py',
+            'is_valid_filename_pyh', 'header_statement', 'accelerator_statement',
+            'get_module_name', 'view_tree', 'get_default_path')
+
 pyccel_external_lib = {"mpi4py"             : "pyccel.stdlib.external.mpi4py",
                        "scipy.linalg.lapack": "pyccel.stdlib.external.lapack",
                        "scipy.linalg.blas"  : "pyccel.stdlib.external.blas",
diff --git a/pyccel/utilities/strings.py b/pyccel/utilities/strings.py
index 13deff4a..a067df01 100644
--- a/pyccel/utilities/strings.py
+++ b/pyccel/utilities/strings.py
@@ -5,7 +5,33 @@
 #------------------------------------------------------------------------------------------#
 """ Module containing helper functions for managing strings
 """
+import string
+import random
 
+__all__ = ('random_string', 'create_incremented_string')
+#==============================================================================
+random_selector = random.SystemRandom()
+
+def random_string( n ):
+    """
+    Generate a random string.
+
+    Generate a random string with length n made of lower case characters and digits.
+
+    Parameters
+    ----------
+    n : int
+      The length of the random string.
+
+    Returns
+    -------
+    str
+       The random string.
+    """
+    chars    = string.ascii_lowercase + string.digits
+    return ''.join( random_selector.choice( chars ) for _ in range(n) )
+
+#==============================================================================
 def create_incremented_string(forbidden_exprs, prefix = 'Dummy', counter = 1, name_clash_checker = None):
     """
     Create a new unique string by incrementing a prefix.
diff --git a/pyproject.toml b/pyproject.toml
index e77c3832..45633ba9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,11 +20,11 @@ dependencies = [
     "sympy >= 1.2",
     "termcolor >= 1.0.0",
     "textx >= 2.2",
+    "astunparse >= 1.6.0", # astunparse is only needed for Python3.8, we should use ast.unparse when we drop Python3.8.
 ]
 
 [project.optional-dependencies]
 test = [
-    "astunparse >= 1.6.0",
     "coverage >= 3.1",
     "mpi4py >= 3.0.0",
     "pytest >= 7.0",


Test patch:
diff --git a/tests/epyccel/modules/function_call_before_definition.py b/tests/epyccel/modules/function_call_before_definition.py
new file mode 100644
index 00000000..8354dbc1
--- /dev/null
+++ b/tests/epyccel/modules/function_call_before_definition.py
@@ -0,0 +1,29 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f1():
+    def g1():
+        return x
+    x = 1
+    return g1()
+
+x1 = f1()
+
+def f2():
+    return g2()
+
+def g2():
+    return 2
+
+x2 = f2()
+
+def f3():
+    def fg1():
+        return fg2()
+
+    def fg2():
+        return x
+    x = 3
+    return fg1()
+
+x3 = f3()
+
diff --git a/tests/epyccel/modules/function_call_before_definition_2.py b/tests/epyccel/modules/function_call_before_definition_2.py
new file mode 100644
index 00000000..e4146667
--- /dev/null
+++ b/tests/epyccel/modules/function_call_before_definition_2.py
@@ -0,0 +1,9 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+def f():
+    return g()
+
+def g():
+    return 2
+
+a = f()
+
diff --git a/tests/epyccel/modules/multi_rank.py b/tests/epyccel/modules/multi_rank.py
index 09d3e482..25909019 100644
--- a/tests/epyccel/modules/multi_rank.py
+++ b/tests/epyccel/modules/multi_rank.py
@@ -24,6 +24,9 @@ def augsub_mixed_order(a : 'float[:,:]', b : 'float[:,:](order=F)'):
 def augdiv_mixed_order(a : 'float[:,:]', b : 'float[:,:](order=F)'):
     a[:] /= b
 
+def add_mixed_order_ellipsis(a : 'float[:,:]', b : 'float[:,:](order=F)'):
+    a[...] = a + b
+
 def mul_by_vector_C(a : 'int[:,:]', b : 'int[:]'):
     a[:] *= b
 
diff --git a/tests/epyccel/test_epyccel_multi_rank.py b/tests/epyccel/test_epyccel_multi_rank.py
index b485984f..49959a31 100644
--- a/tests/epyccel/test_epyccel_multi_rank.py
+++ b/tests/epyccel/test_epyccel_multi_rank.py
@@ -13,7 +13,8 @@ from pyccel.epyccel import epyccel
     multi_rank.augadd_mixed_order,
     multi_rank.augmul_mixed_order,
     multi_rank.augsub_mixed_order,
-    multi_rank.augdiv_mixed_order])
+    multi_rank.augdiv_mixed_order,
+    multi_rank.add_mixed_order_ellipsis])
 def test_add_mixed_order(f1, language):
     f2 = epyccel( f1, language = language )
 
diff --git a/tests/epyccel/test_function_call_before_definition.py b/tests/epyccel/test_function_call_before_definition.py
new file mode 100644
index 00000000..8f3a12b7
--- /dev/null
+++ b/tests/epyccel/test_function_call_before_definition.py
@@ -0,0 +1,25 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+import pytest
+import modules.function_call_before_definition as mod1
+import modules.function_call_before_definition_2 as mod2
+
+from pyccel.epyccel import epyccel
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = pytest.mark.fortran),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="Functions in functions not implemented in c"),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_fun_1(language):
+    modnew = epyccel(mod1, language = language)
+    assert mod1.x1 == modnew.x1
+    assert mod1.x2 == modnew.x2
+    assert mod1.x3 == modnew.x3
+
+def test_fun_2(language):
+    modnew = epyccel(mod2, language = language)
+    assert mod2.a == modnew.a
diff --git a/tests/errors/semantic/blocking/INCOMPATIBLE_ARGUMENT3.py b/tests/errors/semantic/blocking/INCOMPATIBLE_ARGUMENT3.py
new file mode 100644
index 00000000..cde556d3
--- /dev/null
+++ b/tests/errors/semantic/blocking/INCOMPATIBLE_ARGUMENT3.py
@@ -0,0 +1,9 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+from pyccel.decorators import inline
+
+@inline
+def add(a: int, b: 'int | complex'):
+    return a + b
+
+z = add(3., 2.)
diff --git a/tests/errors/semantic/non_blocking/functions_in_uniontype.py b/tests/errors/semantic/blocking/functions_in_uniontype.py
similarity index 100%
rename from tests/errors/semantic/non_blocking/functions_in_uniontype.py
rename to tests/errors/semantic/blocking/functions_in_uniontype.py
diff --git a/tests/pyccel/scripts/decorators_inline.py b/tests/pyccel/scripts/decorators_inline.py
index e37d17a7..8646220c 100644
--- a/tests/pyccel/scripts/decorators_inline.py
+++ b/tests/pyccel/scripts/decorators_inline.py
@@ -1,20 +1,25 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 import numpy as np
-from pyccel.decorators import inline
+from pyccel.decorators import inline, template
 
 pi = 3.14159
 
+@inline
+@template(name='T', types=['int', 'float'])
+def add(a:'T',b:'T'):
+    return a+b
+
 @inline
 def get_powers(s : int):
     return s, s*s, s*s*s
 
 @inline
-def power_4(s : int):
+def power_4(s : 'int|float'):
     tmp = s*s
     return tmp*tmp
 
 @inline
-def f(s : int):
+def f(s : 'int|float'):
     return power_4(s) / 2
 
 @inline
@@ -32,16 +37,39 @@ def not_inline():
 
 @inline
 def positron_charge():
+    # comment
     return -not_inline()
 
+@inline
+@template(name='T',types=['int8[:]','int8[:,:]','int8[:,:,:]','int8[:,:,:,:]','int8[:,:,:,:,:]','int8[:,:,:,:,:,:]',\
+                           'int16[:]','int16[:,:]','int16[:,:,:]','int16[:,:,:,:]','int16[:,:,:,:,:]','int16[:,:,:,:,:,:]',\
+                           'int32[:]','int32[:,:]','int32[:,:,:]','int32[:,:,:,:]','int32[:,:,:,:,:]','int32[:,:,:,:,:,:]',\
+                           'int64[:]','int64[:,:]','int64[:,:,:]','int64[:,:,:,:]','int64[:,:,:,:,:]','int64[:,:,:,:,:,:]',\
+                           'float32[:]','float32[:,:]','float32[:,:,:]','float32[:,:,:,:]','float32[:,:,:,:,:]','float32[:,:,:,:,:,:]',\
+                           'float64[:]','float64[:,:]','float64[:,:,:]','float64[:,:,:,:]','float64[:,:,:,:,:]','float64[:,:,:,:,:,:]'])
+def add2(a : 'T', b : 'T', c:'T'):
+    c[...] = a+b
+
 if __name__ == '__main__':
     print(get_powers(3))
     a,b,c = get_powers(4)
     print(a,b,c)
     print(power_4(5))
     print(f(3))
+    print(f(3.))
     print(sin_base_1(0.5))
     print(sin_base_1(0.7))
     arr = np.empty(4)
     fill_pi(arr)
     print(arr)
+    print(add(1,2))
+    print(add(1.,2.))
+    a1 = np.ones(10)
+    b1 = np.ones(10)
+    c1 = np.zeros(10)
+    a2 = np.ones((10,10))
+    b2 = np.ones((10,10))
+    c2 = np.zeros((10,10))
+    add2(a1, b1, c1)
+    add2(a2, b2, c2)
+    add2(a2, b2, c2)
diff --git a/tests/pyccel/scripts/functions.py b/tests/pyccel/scripts/functions.py
index 0084e8ea..8bb80fa4 100644
--- a/tests/pyccel/scripts/functions.py
+++ b/tests/pyccel/scripts/functions.py
@@ -44,7 +44,9 @@ def f2(x : int, m : int = None):
 def my_print(a : 'int[:]'):
     print(a)
 
-
+def f(a : int = 3, b : int = 3):
+    pass
+b = 1
 y = decr(2)
 z = f1(1)
 
@@ -63,3 +65,4 @@ if __name__ == '__main__':
     print(z1)
     print(z2)
     my_print(np.array([1,2,3]))
+    f(1,b=b)