diff --git a/.DS_Store b/.DS_Store
index 977dfb2ca3e10ade31ef08d0bf14d75b78cbd7ac..28a601ed998575079069aed0e8da4e8a9e53a217 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/Tp09/ap_decorators.py b/Tp09/ap_decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f4adc9129d3d7ffef9d7dc68b59e4d6643d419d
--- /dev/null
+++ b/Tp09/ap_decorators.py
@@ -0,0 +1,135 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+"""
+:module: ap_decorators  
+:author: FIL - Faculté des Sciences et Technologies -  Univ. Lille <http://portail.fil.univ-lille1.fr>_
+:date: 2018, september
+
+"""
+
+from functools import wraps
+
+
+def trace(fct):
+    '''
+    Decorator for tracing every call to fct.
+    Recursive calls are indented.
+
+    :Example:
+
+    >>> @trace
+    ... def fact(n):
+    ...     if n == 0:
+    ...         return 1
+    ...     else:
+    ...         return n * fact(n - 1)
+    
+    >>> fact(5)
+     -> fact((5,), {})
+    ... -> fact((4,), {})
+    ...... -> fact((3,), {})
+    ......... -> fact((2,), {})
+    ............ -> fact((1,), {})
+    ............... -> fact((0,), {})
+    ............... <- 1
+    ............ <- 1
+    ......... <- 2
+    ...... <- 6
+    ... <- 24
+    <- 120
+    120
+    '''
+    @wraps(fct)
+    def wrapper(*args, **kwargs):
+        dots = '...' * wrapper.__depth
+        print('{:s} -> {:s}{:s}'.format(dots, wrapper.__name__, repr((args, kwargs))))
+        wrapper.__depth += 1
+        y = fct(*args, **kwargs)
+        wrapper.__depth -= 1
+        print('{:s} <- {:s}'.format(dots, repr(y)))
+        return y
+    wrapper.__depth = 0
+    return wrapper
+
+def count(fct):
+    '''
+    decorator for counting  calls to  function fct
+    
+    :Example:
+
+    >>> @count
+    ... def fact(n):
+    ...     if n == 0:
+    ...         return 1
+    ...     else:
+    ...         return n * fact(n - 1)
+    
+    >>> fact.counter
+    0
+    >>> fact(5)
+    120
+    >>> fact.counter
+    6
+    '''
+    @wraps(fct) 
+    def wrapper(*args, **kwargs):
+        y = fct(*args, **kwargs)
+        wrapper.counter += 1
+        return y
+    wrapper.counter = 0
+    return wrapper
+
+
+def memoize(fct):
+    '''
+    decorator for memoizing computed values of  function fct
+    
+    :Example:
+
+    >>> @count
+    ... @memoize
+    ... def fact(n):
+    ...     if n == 0:
+    ...         return 1
+    ...     else:
+    ...         return n * fact(n - 1)
+    
+    >>> fact.counter
+    0
+    >>> fact(5)
+    120
+    >>> fact.counter
+    6
+    >>> fact.counter = 0
+    >>> fact(5)
+    120
+    >>> fact.counter
+    1
+    '''
+    cache = dict()
+    @wraps(fct)
+    def wrapper(*args, **kwargs):
+        key = repr((args, kwargs))
+        if key in cache:
+            return cache[key]
+        else:
+            y = fct(*args, **kwargs)
+            cache[key] = y
+            return y
+    return wrapper
+
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS, verbose=False)
+    
+    
+
+
+
+
+
+
+
diff --git a/Tp09/apl1test.py b/Tp09/apl1test.py
new file mode 100644
index 0000000000000000000000000000000000000000..8533ccaca5d99e7cfb83d6d86aa9334bb6a73a40
--- /dev/null
+++ b/Tp09/apl1test.py
@@ -0,0 +1,89 @@
+import thonnycontrib
+from thonnycontrib.backend.evaluator import Evaluator
+import thonnycontrib.backend.l1test_backend
+from thonny.plugins.cpython_backend.cp_back import MainCPythonBackend
+import thonnycontrib.backend.doctest_parser 
+from thonnycontrib.backend.doctest_parser import ExampleWithExpected, ExampleWithoutExpected
+import thonnycontrib.backend.ast_parser
+from thonnycontrib.backend.ast_parser import L1DocTest
+import thonnycontrib.backend.verdicts
+from thonnycontrib.backend.verdicts.ExceptionVerdict import ExceptionVerdict 
+
+import inspect
+import tempfile
+import os
+import sys
+        
+class MockBackend(MainCPythonBackend):
+    """
+    Fake backend.
+    """
+    def __init__(self):
+        ...
+
+    def send_message(self, msg) -> None:
+        ...
+
+# register backend
+thonnycontrib.backend.l1test_backend.BACKEND = MockBackend()
+
+def l1test_to_org(filename: str, source: str=""):
+    """
+    Return an org abstract of the tests presents in `filename` file.
+    """
+    abstract = {'total': 0,
+                'success': 0,
+                'failures': 0,
+                'errors': 0,
+                'empty': 0}
+
+    if source == "":
+        with open(filename, 'rt') as fin:
+            source = fin.read()
+    evaluator = Evaluator(filename=filename,
+                          source=source)
+    tests = evaluator.evaluate()
+    n = len(tests)
+    abstract['total'] = n
+    res = ""
+    for test in tests:
+        examples = test.get_examples()
+        res_examples = ""
+        nb_test, nb_test_ok = 0, 0
+        empty = True
+        for example in examples:
+            verdict = test.get_verdict_from_example(example)
+            if isinstance(example, ExampleWithExpected):
+                nb_test += 1
+                if verdict.isSuccess():
+                    nb_test_ok += 1
+                    abstract['success'] += 1
+                else:
+                    abstract['failures'] += 1
+                empty = False
+            if isinstance(verdict, ExceptionVerdict):
+                abstract['errors'] += 1
+                empty = False
+            res_examples += f"** {verdict}\n\n"
+            if not verdict.isSuccess():
+                res_examples += f"   {verdict.get_details()}\n\n"
+        if not empty: 
+            res += f"* {test.get_name()} ~ {nb_test_ok}/{nb_test} réussis\n\n"
+        else:
+            abstract['empty'] += 1
+            res += f"* {test.get_name()}\n\n Aucun test trouvé !\n\n"
+        res += res_examples
+    res = f"Tests exécutés : {abstract['total']}\nSuccès: {abstract['success']}, \
+Echecs: {abstract['failures']}, Erreurs: {abstract['errors']}, \
+Vide: {abstract['empty']}\n\n" + res
+    return res
+
+
+def testmod(modulename: str):
+    """
+    mimic the doctest.testmod function
+    for `modulename` module
+    """
+    print(l1test_to_org(modulename))
+
+
diff --git a/Tp09/aplst.py b/Tp09/aplst.py
new file mode 100755
index 0000000000000000000000000000000000000000..3fcca937a0a806af3ea8c780d9dcb84ce5017e1d
--- /dev/null
+++ b/Tp09/aplst.py
@@ -0,0 +1,141 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+"""
+:mod:`list` module
+
+:author: FIL - Faculté des Sciences et Technologies - 
+         Univ. Lille <http://portail.fil.univ-lille1.fr>_
+
+:date: 2022, march
+:last revision: 2024, march
+
+Provides 
+
+- a class :class:`ApLst` for non mutable lists
+- an exception :class:`ApLstError` 
+
+
+ApLsts are either empty, either essentially objects with two composants :
+
+#. a *head* composant which represent the head value  of the list,
+#. and a *tail* composant which is represent the tail of the list
+
+"""
+
+
+class ApLstError(Exception):
+    """
+    Exception used by methods
+
+    * ``__init__``
+    * ``head``
+    * ``tail``
+    
+    of class :class:`ApLst`.
+    """
+    def __init__(self, msg):
+        self.message = msg
+
+
+class ApLst():
+    """
+    $$$ list = ApLst()
+    $$$ list.is_empty()
+    True
+    $$e list.head()
+    ApLstError
+    $$$ list2 = ApLst(1, list)
+    $$$ list2.is_empty()
+    False
+    $$$ list2.head()
+    1
+    $$$ list2.tail().is_empty()
+    True
+    $$$ l = ApLst(2, list2)
+    $$$ repr(l)
+    'ApLst(2, ApLst(1, ApLst()))'
+    $$$ str(l)
+    '[2, 1]'
+    $$$ repr(list2)
+    'ApLst(1, ApLst())'
+    """
+    
+    def __init__(self, *args):
+        """
+        build a new empty list if args is empty, 
+        or a list whose head is first element of args, 
+        and tail list is second element of args.
+    
+        précondition: len(args) in {0, 2} 
+             and if len(args) == 2, args[1] must be a ApLst
+    
+        raise: `ApLstError` if précondition is not satisfied
+        """
+        if len(args) == 0:
+            self.content = ()
+        elif len(args) == 2:
+            if isinstance(args[1], ApLst):
+                self.content = (args[0], args[1])
+            else:
+                raise ApLstError('bad type for second argument')
+        else:
+            raise ApLstError('bad number of arguments')
+
+    def is_empty(self) -> bool:
+        """
+        return 
+           - True if self is empty
+           - False otherwise
+        précondition: none
+        """
+        return len(self.content) == 0
+
+    def head(self):
+        """
+        return: head element of self
+        raise: `ApLstError` if self is empty
+        """
+        if self.is_empty():
+            raise ApLstError('head: empty list')
+        else:
+            return self.content[0]
+
+    def tail(self) -> "ApLst":
+        """
+        return: tail list of self
+        raise: `ApLstError` if self is empty
+        """
+        if self.is_empty():
+            raise ApLstError('head: empty list')
+        else:
+            return self.content[1]
+        
+    def __str__(self) -> str:
+        """
+        return: a string representation of list self
+        précondition: none
+        """
+        def str_content(self, item_number=0):
+            if self.is_empty():
+                return ''
+            elif item_number == 50:
+                return ', ...'
+            else:
+                comma = '' if item_number == 0 else ', '
+                return (comma + str(self.head()) +
+                        str_content(self.tail(), item_number + 1))
+                
+        return f'[{str_content(self)}]'
+
+    def __repr__(self) -> str:
+        if self.is_empty():
+            content = ""
+        else:
+            content = f"{repr(self.head())}, {repr(self.tail())}"
+        return f"ApLst({content})"
+
+   
+if (__name__ == '__main__'):
+    import apl1test
+    apl1test.testmod("aplst.py")
diff --git a/Tp09/merge_sort.py b/Tp09/merge_sort.py
new file mode 100755
index 0000000000000000000000000000000000000000..211389d2ed13b0eea43838460eb347975b103d3f
--- /dev/null
+++ b/Tp09/merge_sort.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+"""
+:mod:`recursive sorts`
+:author: `FIL - FST - Univ. Lille.fr <http://portail.fil.univ-lille1.fr>`_
+:date: 2016, september. Last revised: 2018, september
+
+Some recursive sorting algorithms:
+
+- quicksort
+- mergesort
+
+"""
+from typing import Callable, TypeVar
+from aplst import ApLst
+
+
+T = TypeVar('T')
+
+
+def compare(a: T, b: T) -> int:
+    """
+    return:
+       - -1 if a < b
+       -  1 if a > b
+       -  0 if a = b
+    precondition: a and b must be comparable with <
+    exemples:
+
+    $$$ compare(0, 1)
+    -1
+    $$$ compare('a', 'a')
+    0
+    $$$ compare((2, 1), (1, 2))
+    1
+    """
+    if a < b:
+        return -1
+    elif a > b:
+        return 1
+    else:
+        return 0
+
+
+def length(li: ApLst) -> int:
+    """
+    return the length of li.
+
+    precondition: none
+
+    examples:
+
+    $$$ length(ApLst())
+    0
+    $$$ length(ApLst(3, ApLst(1, ApLst(4, ApLst()))))
+    3
+    """
+    if not li.is_empty():
+        res = 1 + lenght(li.tail())
+    else:
+        res = 0
+    return res
+
+
+def native_to_list(li: list[T]) -> ApLst:
+    """
+    return a recursive list containing the same element of li.
+
+    precondition: none
+
+    examples:
+
+    $$$ native_to_list([]).is_empty()
+    True
+    $$$ rec_lst = native_to_list([3, 1, 4, 1, 5])
+    $$$ length(rec_lst)
+    5
+    $$$ rec_lst.head()
+    3
+    $$$ l = rec_lst.tail()
+    $$$ l.head()
+    1
+    $$$ l = l.tail()
+    $$$ l.head()
+    4
+    """
+    
+
+
+def list_to_native(li: ApLst) -> list[T]:
+    """
+    return a native python list containing the same element of li.
+
+    precondition: none
+
+    examples:
+
+    $$$ list_to_native(ApLst())
+    []
+    $$$ list_to_native(ApLst(3, ApLst(1, ApLst(4, ApLst(1, ApLst(5, ApLst()))))))
+    [3, 1, 4, 1, 5]
+    """
+    ...
+
+
+def is_sorted(l: ApLst, comp: Callable[[T, T], int]=compare) -> bool:
+    """
+    return True if list l is sorted by ascending order
+    and False otherwise.
+
+    precondition: elements of l must be comparable
+    exemples:
+
+    $$$ is_sorted(native_to_list([1, 2, 3, 4]))
+    True
+    $$$ is_sorted(native_to_list([1, 2, 4, 3]))
+    False
+    """
+    ...
+
+
+def split(l: ApLst) -> tuple[ApLst, ApLst]:
+    """
+    return a couple (l1,l2) of lists of equal length
+
+    exemples:
+
+    $$$ l = [3, 1, 4, 1, 5, 9, 2]
+    $$$ l1, l2 = split(native_to_list(l))
+    $$$ abs(length(l1) - length(l2)) <= 1
+    True
+    $$$ l3 = list_to_native(l1) + list_to_native(l2)
+    $$$ len(l3) == len(l)
+    True
+    $$$ all(k in l for k in l3)
+    True
+    """
+    ...
+
+
+def merge(l1: ApLst, l2: ApLst,
+          comp: Callable[[T, T], int]=compare) -> ApLst:
+    """
+    return a list containing all elements de l1 and l2.
+    If l1 and l2 are sorted, so is the returned list.
+
+    precondition: elements of l1 and l2 are comparable
+    exemples:
+
+    $$$ list_to_native(merge(native_to_list([1, 3, 4, 9]), native_to_list([1, 2, 5])))
+    [1, 1, 2, 3, 4, 5, 9]
+    """
+    ...
+
+
+def mergesort(l: ApLst, comp: Callable[[T, T], int]=compare) -> ApLst:
+    """
+    return a new list containing elements of l sorted by ascending order.
+
+    precondition: elements of l are comparable
+    exemples:
+
+    $$$ list_to_native(mergesort(native_to_list([3, 1, 4, 1, 5, 9, 2])))
+    [1, 1, 2, 3, 4, 5, 9]
+    $$$ import random
+    $$$ n = random.randrange(20)
+    $$$ l = native_to_list([random.randrange(20) for k in range(n)])
+    $$$ l1 = mergesort(l)
+    $$$ length(l1) == length(l)
+    True
+    $$$ is_sorted(l1)
+    True
+    """
+    ...
+
+
+if (__name__ == '__main__'):
+    import apl1test
+    apl1test.testmod("merge_sort.py")
+
+
+
diff --git a/Tp09/merge_sort_analysis.py b/Tp09/merge_sort_analysis.py
new file mode 100644
index 0000000000000000000000000000000000000000..e93962c1f2b985724e9f71d40666aa9364afa043
--- /dev/null
+++ b/Tp09/merge_sort_analysis.py
@@ -0,0 +1,92 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+"""
+:mod:`mergesort_analysis` module
+:author: FIL - FST - Univ. Lille <http://portail.fil.univ-lille.fr>_
+:date: 2024, April. 
+
+Experimental analysis of mergesort algorithm
+
+"""
+from aplst import ApLst
+from merge_sort import compare, mergesort, native_to_list
+from ap_decorators import count
+
+
+import random
+import matplotlib.pyplot as plt
+from math import log
+
+def random_list(size: int) -> ApLst:
+    '''
+    :param size: (int)
+    :return: (list) list of size size containing all natural numbers from 0 to n-1 in random order
+    :CU: n >= 0
+    '''
+    l = list(range(size))
+    random.shuffle(l)
+    return native_to_list(l)
+
+
+def myplot(listX: list[float], listY: list[float],
+           title: str='', xlabel: str ='', ylabel: str=''):
+    '''
+    plot the data in listX and listY
+    '''
+    plt.plot(listX, listY)
+    plt.title(title)
+    plt.xlabel(xlabel)
+    plt.ylabel(ylabel)
+    plt.show()
+
+
+def build_comp_number_list(sort, max_size: int, sample_size: int) -> list[float]:
+    '''
+    return: list of average number of comparisons for sorting lists of size up to max_size
+    '''
+    comp = count(compare)
+    nb_comps = []
+    for size in range(1, max_size):
+        comp.counter = 0
+        for _ in range(sample_size):
+            sort(random_list(size), comp=comp)
+        nb_comps.append(comp.counter / sample_size)
+    return nb_comps
+
+
+def usage():
+    print('Usage: {:s} <max size> <sample size>'.format(sys.argv[0]),
+          file=sys.stderr)
+    print('\t<max size> = max size of lists to sort', file=sys.stderr)
+    print('\t<sample size> = size of samples.', file=sys.stderr)
+    exit(1)
+    
+if __name__ == '__main__':
+    import sys
+    
+    if len(sys.argv) != 3:
+        print('Bad number of arguments!', file=sys.stderr)
+        usage()
+
+    try:
+        MAX_SIZE = int(sys.argv[1])
+    except ValueError:
+        print('Max size must be an integer!', file=sys.stderr)
+        usage()
+
+    SIZES = list(range(1, MAX_SIZE))
+    
+    try:
+        SAMPLE_SIZE = int(sys.argv[2])
+    except ValueError:
+        print('Sample size must be an integer!', file=sys.stderr)
+        usage()
+
+    # all is OK!
+    NB_COMPS = build_comp_number_list(mergesort, MAX_SIZE, SAMPLE_SIZE)
+    myplot(SIZES, NB_COMPS,
+           title=f'Nbre de comparaisons du tri fusion (mesuré sur échantillon de taille {SAMPLE_SIZE})',
+           xlabel='taille des listes',
+           ylabel='nbre de comparaisons')
+