Site logo

Triceraprog
La programmation depuis le Crétacé

  • Forth sur 6502, épisode 13 ()

    Les mots nécessaires à l'interprétation

    À partir de ce qui a été établi dans l'article précédent, il est à présent temps d'implémenter les mots nécessaires directement ou indirectement pour INTERPRET.

    HERE

    HERE est un mot qui indique quelle est la prochaine adresse libre dans l'espace de travail de l'interpréteur Forth. Il met en fait sur la pile le contenu d'une variable nommée DP qui pointe vers cette adresse. Cet emplacement se situe après le dictionnaire, et donc va se déplacer au fur et à mesure de l'ajout des mots.

    L'implémentation est simple, cependant, cela signifie aussi de s'intéresser à l'emplacement de travail. Actuellement, on travaille sur la zone de 2ko de RAM disponible sur la Famicom. C'est probablement suffisant pour quelques tests. Le linker permet de savoir quel est le premier octet libre après la zone BSS (variables non initialisées). C'est vers là que je fais pointer DP lors de l'initialisation du système.

    Avec une cartouche contenant de la RAM supplémentaire CPU, il sera possible de déplacer cette zone de travail.

    LATEST

    Trouver un mot dans le dictionnaire a besoin de connaître l'adresse du dernier mot ajouté, afin de remonter toute la chaîne de mots définis. J'ai déjà une variable que j'utilisais jusqu'à maintenant uniquement dans les tests, pour vérifier la présence d'une chaîne valide de dictionnaire. Il suffit donc de rendre publique cette variable.

    WORD

    Un mot un peu moins simple à implémenter... mais surtout comment le tester ? Avec INTERPRET implémenté ça serait facile mais justement, c'est ce que je suis en train d'implémenter. Avec mon harnais de tests en lua, voici l'idée : injecter du code Forth en RAM et y router, grâce à un breakpoint judicieusement placé, le chaînage d'instructions du Forth (assuré par next). La suite d'instructions se termine par une boucle infinie le temps d'effectuer les relevés de tests.

    C'est un peu acrobatique, mais ça fonctionne. Du moins tant que je ne teste rien en rapport avec le PPU.

    Et je me retrouve à écrire, pour faciliter l'écriture des tests, une sorte de mini INTERPRET (en mode compilation) en LUA :

        local cells = {}
        for _, item in ipairs(self.thread) do
            if type(item) == "string" then
                table.insert(cells, resolveCFA(item))
            else
                table.insert(cells, item & 0xFFFF)
            end
        end
    

    Ce qui permet de transformer en thread Forth une table LUA contenant quelque chose de ce type :

    {"LIT", 0x1234, "LIT", 0x5678, "DROP"}
    

    Et par la même occasion, je peux vérifier que LATEST, HERE et DP renvoient les valeurs attendues !

    Et à propos du choix sur la condition d'arrêt de la boucle d'interprétation, j'ai finalement choisi que ce soit lorsque WORD n'a plus rien à parser dans le buffer d'entrée. Dans ce cas là, il laisse dans HERE un mot de longueur nulle, qui sera le signal pour la boucle d'interprétation de s'arrêter.

    COUNT

    Ce mot prend l'adresse d'une chaîne préfixée par sa longueur, tel que renvoyé par WORD par exemple, et met sur la pile l'adresse des données puis la longueur. Cela permet d'être traité par d'autres mots. Il faut bien entendu veiller à ce que l'adresse reste valide. Dans le contexte d'interprétation, ce contexte doit rester valide jusqu'à trouver le mot ou le transformer en nombre. Comme les mots sont traités un par un et ne génèrent pas de nouveau mot dans le dictionnaire, c'est bon.

    Pas de difficulté particulière pour ce mot très simple.

    CAPITAL

    Puis vient CAPITAL, un mot qui met une chaîne décrite sur la pile en majuscules. Le but ici est de rendre la recherche de mots dans le dictionnaire non sensible à la casse. Là encore, pas de difficulté particulière : il suffit de parcourir la chaîne sur la longueur spécifiée et de transformer les lettres trouvées.

    (FIND)

    Ce mot est le mot qui sert à trouver un mot dans le dictionnaire. Il est utilisé par -FIND, qui s'occupe aussi de récupérer le mot et le mettre en forme.

    Encore un mot que j'écris en assembleur. Il est appelé souvent et je n'ai pas encore tous les mots nécessaires pour l'implémenter : pour remonter la chaîne et trouver le LFA, il y a besoin d'un peu d'arithmétique. Je n'ai pas encore ces mots là.

    -FIND

    C'est le mot qui va chercher la prochaine chaîne valide, la met en majuscules puis cherche le mot. Ici, j'ai tous les mots, je peux donc l'écrire en Forth. Cependant, avec mes macros actuelles, je ne peux pas définir un mot Forth qui commence par un caractère spécial, car cela génère automatiquement des labels et ceux-ci seraient invalides.

    Tout comme je l'avais fait pour les mots écrits en assembleur, j'ajoute donc une macro qui permet de donner un nom « interne au compilateur » à ces mots valides en Forth.

    Et voici son code, qui utilise tous les mots implémentés précédemment ! Ça prend forme :

    : -FIND
        BL WORD             \ cherche le mot se terminant par un délimiteur espace dans le TIB
        HERE COUNT CAPITAL  \ passe ce mot en majuscules
        HERE LATEST (FIND)  \ cherche à partir de LATEST et laisse le résultat sur la pile
        ;
    

    On remarque que -FIND a la même sortie sur pile que (FIND), puisque c'est le dernier mot appelé, et que lui-même n'ajoute rien à, ni ne retire rien de la pile.

    À noter : si TIB ne contient plus de mot, WORD laisse une chaîne de taille nulle à HERE, qui ne sera pas trouvée par (FIND). Il faudra donc vérifier, lorsqu'un mot n'est pas trouvé, si par hasard ce n'était pas le mot de taille nulle.

    C'est ici que l'astuce des premiers Forth avait un mot de taille nulle qui provoquait la sortie de la boucle d'interprétation. Cette astuce était mentionnée dans le précédent article, avec ce qu'en pensait la co-autrice de Forth. C'est en suivant son conseil que je n'ai pas suivi cette voie.

    CFA

    Ce mot donne le CFA d'un mot à partir d'un PFA. -FIND donne le PFA et EXECUTE a besoin du CFA, il faut donc transformer l'un en l'autre. Le passage de l'un à l'autre est très simple dans cette implémentation : il suffit de soustraire 2 à l'adresse.

    EXECUTE

    Ce mot prend un CFA et lance l'exécution correspondant à ce CFA. Pas de difficulté particulière, il suffit d'appeler NEXT mais avec le CFA pris depuis la pile.

    Pour tester le mot, il me faut un mot simple avec un effet simple à vérifier. N'importe quel mot simple qui met une valeur sur la pile fera l'affaire. Pourquoi pas LATEST. En prenant le CFA de LATEST via le harnais de test, puis en faisant EXECUTE, je dois retrouver l'adresse de LATEST sur la pile.

    Allez presque !

    Presque tous les mots pour l'implémentation de INTERPRET sont là. La plupart étaient simples ; il reste cependant un gros morceau. Voici mon objectif de code pour INTERPRET pour le moment :

    : INTERPRET
      BEGIN
        -FIND IF ( WORD FOUND )
          CFA EXECUTE
        ELSE
          HERE C@
          IF
            HERE NUMBER
          ELSE
            EXIT
          ENDIF
        ENDIF
      AGAIN ;
    

    Je rappelle que seul le mode d'interprétation est pris en compte, pas le mode de compilation, pas encore.

    BEGIN, IF, ELSE, ENDIF, AGAIN et EXIT sont des mots immédiats qui fonctionneront quand le mode compilation sera actif. Pour le moment, puisque je code ces mots directement avec les CFA, via des macros assembleurs, je n'ai besoin que de 0BRANCH.

    Il reste aussi un C@ qui est le pendant en octet de @, qui sera trivial à implémenter si le code reste le même.

    Mais pour l'objectif qui est toujours de pouvoir exécuter HEX 7FF FF C!, le plus difficile, c'est NUMBER. Transformer des nombres écrits comme des chaînes de caractères en représentation interne, dans n'importe quelle base, ça représente un peu de code.

    Cela sera donc le sujet du prochain épisode.


  • Forth sur 6502, épisode 12 ()

    La reprise

    L'écriture de cet article a commencé le 3 mars 2026... et je le reprends le 9 juillet de la même année. Heureusement que j'avais déjà laissé pas mal de notes car la reprise est difficile !

    Interpréter une ligne

    Depuis l'article précédent, la boucle principale du système Forth est capable de récupérer une ligne de texte entrée par l'utilisateur. L'étape suivante est bien entendu d'en faire quelque chose : de l'interpréter.

    INTERPRET est le mot Forth qui s'occupe de cela. Et ce que fait ce mot est assez simple : prendre le prochain morceau de texte entouré d'espace disponible dans la ligne, essayer de le trouver dans le dictionnaire, et si c'est le cas, l'exécuter. Si le mot n'est pas trouvé, la boucle tente de l'interpréter comme un nombre, et si c'est un nombre valide, elle le pousse sur la pile. Et si ce n'est pas un nombre, alors c'est une erreur.

    Du moins, c'est le fonctionnement de INTERPRET en mode d'interprétation directe. INTERPRET peut aussi fonctionner en mode de compilation. Mais nous verrons cela plus tard. Pour le moment, je veux juste une interprétation directe afin de pouvoir exécuter la ligne Forth suivante : HEX 7FF FF C!.

    Voilà à quoi ressemble INTERPRET (avec un nommage FIG-Forth) avec uniquement le mode interprétation directe :

    : INTERPRET
        BEGIN
            -FIND IF
                CFA EXECUTE
            ELSE
                HERE NUMBER
            ENDIF
        AGAIN
    

    Il y a quelques mots ici que je vais devoir définir. Tout d'abord -FIND, qui a la charge de prendre le prochain groupe de caractères depuis le TIB (Terminal Input Buffer) et de trouver s'il y a un mot correspondant dans le dictionnaire. Si un mot est trouvé, alors le PFA (Parameter Field Address) est laissé sur la pile.

    CFA est un mot qui trouve le CFA (Code Field Address) d'un mot à partir de son PFA. Et EXECUTE est un mot qui exécute le code à l'adresse donnée sur la pile. On voit donc comment l'enchaînement de ces trois mots permet d'exécuter un mot trouvé dans le dictionnaire.

    -FIND a un effet de bord : lors du parsing du mot, il laisse celui-ci dans la zone de données pointée par HERE. Cette zone de données est une zone libre en RAM. Incidemment celle où serait construit un mot si on était en mode de compilation. Mais dans notre cas, c'est une zone libre d'usage. Ainsi, dans le cas où -FIND ne trouve pas de mot correspondant dans le dictionnaire, il laisse les caractères à interpréter dans cette zone de données. NUMBER prend une adresse et tente d'interpréter les caractères à partir de cette adresse comme un nombre. Si c'est un nombre valide, il le pousse sur la pile. Sinon, c'est une erreur qui fera sortir de la boucle d'interprétation.

    Sortir d'une boucle infinie

    Mais comment est-ce que l'on sort de cette boucle ? En effet, INTERPRET doit redonner la main une fois la ligne complètement interprétée. L'astuce de Fig-Forth est d'ajouter au dictionnaire un mot nommé X, dont le nom est de longueur 1 et de valeur 0. Ainsi, lorsque -FIND arrive en fin de ligne, il trouve (à travers WORD) ce mot X et l'exécute. Or, X est défini de manière à faire sortir de la boucle d'interprétation en enlevant l'adresse de retour qui est au sommet de la pile des retours.

    Je ne sais pas encore si je vais utiliser cette astuce. Elle a une sorte d'élégance pratique, mais aussi un côté « hack ». Voici ce qu'en dit la co-autrice de Forth, Elizabeth Rather dans cette discussion sur comp.lang.forth le 5 octobre 2011 :

    *Sigh* I remember that trick. It was in very early Forths, probably as
    long ago as NRAO. Awful. Excessively "cute" and obscure. Once you're
    done enjoying your "eureka!" moment, forget you ever saw that!
    
    Traduction française : *soupir* Je me souviens de cette astuce. Elle était dans les tout premiers Forth, probablement aussi vieille que le NRAO. C'est affreux. Excessivement « mignon » et obscur. Une fois que vous avez fini de profiter de votre moment « eureka ! », oubliez que vous avez jamais vu ça !
    

    Et poursuit en donnant une solution alternative :

    A much cleaner solution is to have BEGIN ... WHILE ... REPEAT loops that
    compile or interpret depending on STATE, with the loops terminating when
    the current input source is exhausted, whereupon the system (or TERMINAL
    task) simply waits for more input in the BEGIN ... AGAIN loop in QUIT.
    
    Traduction française : une solution beaucoup plus propre est d'avoir des boucles BEGIN ... WHILE ... REPEAT qui compilent ou interprètent selon l'état, avec les boucles se terminant lorsque la source d'entrée actuelle est épuisée, auquel cas le système (ou la tâche TERMINAL) attend simplement plus d'entrée dans la boucle BEGIN ... AGAIN de QUIT.
    

    Trouver un mot dans le dictionnaire

    -FIND est le mot le plus complexe à implémenter pour faire fonctionner INTERPRET. Et lui-même appelle plusieurs autres mots. Voici une implémentation possible de -FIND :

    : -FIND ( -- addr len flag )
        BL WORD
        HERE COUNT CAPITAL
        HERE LATEST (FIND)
    

    Le mot agit en trois étapes. Tout d'abord WORD prend le prochain groupe de caractères terminé par le délimiteur spécifié, ici BL, qui représente l'espace. Comme indiqué plus haut, WORD laisse les caractères traités dans la zone de données pointée par HERE, précédés de leur longueur de la chaîne.

    Ensuite, CAPITAL convertit les caractères en majuscules. Ce mot prend en entrée l'adresse de la chaîne à convertir ainsi que sa longueur. C'est le rôle de COUNT que de transformer une chaîne de caractères précédée de sa longueur en une adresse de cette chaîne et une longueur séparées sur la pile.

    Enfin, l'appel à (FIND) cherche dans le dictionnaire un mot dont le nom correspond à la chaîne de caractères donnée en partant de LATEST, qui est une variable qui pointe vers le dernier mot ajouté au dictionnaire. On se souvient que dans cette implémentation, le dictionnaire est une liste chaînée de mots. (FIND) partira donc du dernier ajouté puis va remonter toute la chaîne jusqu'à trouver un mot... ou pas.

    Si le mot n'est pas trouvé, le flag de retour est 0 et il n'y aura ni l'adresse ni la longueur de la chaîne sur la pile.

    Pas de vocabulaire pour le moment

    En plus de la simplification du mot INTERPRET pour lequel je ne traite pas le mode de compilation, je fais une autre simplification : je n'implémente pas les vocabulaires. Un vocabulaire est un mécanisme de Forth qui permet de regrouper des mots dans des espaces de noms. Cela permet de travailler dans un certain contexte avec des mots ayant une certaine signification, et de changer de contexte au besoin.


  • Compression de données, la vidéo ()

    Après ma série de quatre articles sur la compression de données sans pertes commencée l'année dernière, j'ai eu envie de faire une petite vidéo illustrative de ces informations. C'est chose faite, avec un angle assez similaire : donner une intuition, une compréhension générale, de comment fonctionnent ces algorithmes. Mais cette fois avec des images animées, ce que je trouve complémentaire dans les articles.

    Pour rappel, les article sont les suivants :

    Après avoir écrit un premier script incluant la partie ZX0, j'ai finalement décidé de retirer cette partie afin de rester dans la compréhension globale. Parler de ZX0, c'est rentrer dans le détail d'implémentation, ça fait intervenir de nombreux concepts. Ça alourdissait beaucoup la vidéo.

    Et voici la vidéo :


  • TextooM, un jeu pour CP/M ()

    Une fois Micreversi terminé, et avant de revenir sur Family Forth, j'ai eu envie de faire un petit détour par une idée qui me trottait dans la tête depuis un bon moment. Depuis le moment du live de la restauration Micral N où on avait affiché « DOOM » en BASIC pour affirmer : Doom tourne sur Micral N. C'était une blague bien entendu, mais pourquoi pas continuer la blague.

    L'idée était donc de faire un Doom façon aventure textuelle, initialement pour Micral N. Mais après réflexion, comme ce n'est pas une machine très courante et puisque mon émulateur n'est pas très pratique à utiliser, je me suis dit qu'une version CP/M serait plus accessible. Et aussi plus facile à produire.

    Et voici ainsi TextooM, un jeu d'aventure textuelle pour CP/M qui s'inspire du premier niveau de Doom, E1M1. C'est plus un jouet qu'un vrai jeu, il ne faut pas en attendre beaucoup. Mais il y a des secrets à trouver, avec un pourcentage à la fin, et il est possible de rusher, ou de tenter le nettoyage complet du niveau (qui n'est pas si simple).

    De quoi amuser 15 minutes...

    TextooM n'est pas le premier jeu de ce type. Au début du développement, j'ai cherché si cela avait été déjà fait et, oui, cela a été fait. En 1996 même ! Ce jeu est Foom et utilise le moteur de fiction interactive TADS. N'ayant pas envie d'entreprendre un portage de TADS, et Foom ayant beaucoup de texte, peut-être beaucoup trop pour une machine CP/M, j'ai décidé de faire mon propre moteur, plus simple, et de faire un jeu plus court. Ah, et en français.

    Le code source du projet est disponible sur GitLab. Et je vous fournis ici l'exécutable pour CP/M.

    Écran de jeu de TextooM


  • Micreversi, un micro-jeu pour Canon X07 ()

    La session Canon X07 de la Game Jam « Retro Programmers United for Obscure Systems » se termine (officiellement, elle est terminée, mais cette game jam est flexible sur les délais), il est temps de revenir sur ma contribution, un Reversi (ou Othello, mais c'est une marque déposée).

    Le Reversi

    Tout d'abord, qu'est-ce que le Reversi ? C'est un jeu de plateau pour deux joueurs, joué sur une grille de 8x8 cases avec des pions réversibles, généralement noirs d'un côté et blancs de l'autre. Le but du jeu est d'avoir le plus de pions à sa couleur à la fin de la partie.

    Pour retourner des pions adverses, il faut les entourer de ses propres pions. Lors de la pose d'un pion, tous les pions adverses qui sont sur une ligne droite (horizontale, verticale ou diagonale) entre le pion posé et un autre pion de la même couleur sont retournés.

    C'est simple à comprendre, mais pas forcément si simple à jouer. Être trop gourmand au début risque de se retourner contre soi.

    Le Canon X07

    Le Canon X07 est un micro-ordinateur sous forme d'une grosse calculatrice, la version « imposante » des calculatrices programmables des années 80. Il est équipé d'un processeur compatible Z80, mais le gros du boulot graphique et sonore est assuré par un coprocesseur dédié. Il a pas mal de fonctionnalités et de périphériques. C'est une machine que j'aime bien, mais que je n'avais jamais eu l'occasion de programmer.

    Comme toujours avec ces machines, l'état des émulateurs n'est pas glorieux. Quelques participants, dont je fais partie, ont envoyés quelques patchs à MAME, mais il reste encore beaucoup de boulot pour avoir quelque chose d'à peu près fiable. Utiliser la vraie machine était quasi indispensable. Mais en contrepartie, mon programme final ne tourne avec MAME qu'avec une version très récente.

    Merci d'ailleurs à pR-0000 pour son câble série et son chargeur qui a permis des transferts très pratiques vers la machine.

    La source du jeu

    J'ai pas mal tourné en rond avant de trouver une idée qui me plaise et soit adapté à la machine. Je me dirigeais vers un jeu de pions, ce qui a l'avantage de ne pas nécessiter de grosses capacités graphiques, et je me suis souvenu quand dans ma pile de notes, j'avais référencé un programme de Reversi en BASIC publié dans le premier numéro du magazine l’Ordinateur Individuel en Octobre 1978 (pages 50 à 52, signé Jean-Marie Donat).

    Ce programme est assez court, plutôt bien expliqué avec un but pédagogique : apprendre à programmer en BASIC.

    L'idée a donc été d'en faire un portage pour le Canon X07, en ajoutant une interface graphique pour la sélection des pions avec un curseur, puisque le Canon X07 a 4 flèches directionnelles bien visibles. Le programme BASIC d'origine, lui, demande des coordonnées et fait un affichage en mode texte, en affichant à chaque tour la grille de jeu.

    Le BASIC, c'est lent !

    En tout cas, un BASIC Microsoft interprété sur une console construire autour de l'économie d'énergie... La première version complète du jeu, entièrement en BASIC, était injouable. La réflexion de l'ordinateur plusieurs secondes, mais à la rigueur, ce n'était pas le plus gênant. Le déplacement du curseur, prenant plus d'une seconde à chaque mouvement, était lui vraiment injouable.

    J'ai donc décidé de transcrire le tout en assembleur.

    L'assembleur, c'est plus rapide !

    Nettement, nettement plus rapide. Ça a pris un peu de temps à convertir, le temps de retrouver la bonne manière de s'adresser au coprocesseur pour la plupart des opérations. Là-dessus, les actifs du Discord ont été d'une grande aide, merci à eux !

    Partant d'un programme entièrement fonctionnel en BASIC, le reste de la transcription a été relativement automatique. Relativement.

    C'est fun ?

    Selon moi, le jeu se tient à peu près. Il est jouable, j'ai fait quelques parties. Le niveau de l'ordinateur est très basique. Il pourra battre un débutant, mais on peu rapidement prendre le dessus. Sans être un bon joueur de Reversi, je gagne à présent toutes les parties.

    C'était d'ailleurs une proposition de l'article dans le magazine : améliorer le programme. Je l'ai amélioré d'un point de vue utilisation et graphiquement, mais pas du tout d'un point de vue intelligence artificielle. C'est à la base pour cela que je l'avais pris en note. Pour un article potentiel sur ce site. L'idée est toujours là.

    Autre Reversi

    Contrairement à d'autres machines utilisées dans les « Retro Programmers United for Obscure Systems », le Canon X07 a eu droit a beaucoup de listings de jeux en BASIC. Il rentre donc dans la liste des machines avec peu de jeux officiellement exploité durant sa durée de vie, tout en ayant une ludothèque pas si maigre.

    Entre autre, il y a un jeu de Reversi publié page 138 du livre « Applications en assembleur dans l'univers du Canon X-07 ». J'ai tenté de le recopier, malheureusement, le scan auquel j'ai accès est très abîmé sur certaines parties, et le programme est listé comme un code machine en hexadécimal. Cependant, ce que j'ai pu récupérer à l'air de promettre un jeu graphique avec une IA plus développée que celle de l'article de l'Ordinateur Individuel, le tout en 3073 octets (mon programme fait 2460 octets).

    J'espère pouvoir le voir tourner un jour.

    Page du jeu

    Le jeu est disponible sur la page itch.io dédiée.

    J'ai aussi publié le code source de la version assembleur.

    Écran de jeu de Micreversi


Page 1 / 28 (suivant) »

Tous les tags

3d (15), 6502 (15), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (33), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (31), BASIC-80 (4), C (3), Calculs (1), Canon X07 (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), Compression (5), CP/M (1), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Famicom (11), Forth (16), Game Jam (1), Hector (3), Histoire (1), Hooks (4), Huffman (2), i8008 (1), Image (17), Jeu (17), Jeu Vidéo (4), Livre (1), Logo (2), LZ (2), LZ77 (2), Machine virtuelle (2), Magazine (1), MAME (1), Matra Alice (3), MDLC (7), Micral (2), Motorola (1), MSX (1), Musée (2), Nintendo Switch (1), Nombres (3), Optimisation (1), Outil (1), Outils (3), Pascaline (1), Peertube (1), PHC-25 (2), Photo (2), Programmation (21), Python (1), RLE (2), ROM (15), RPUfOS (7), Salon (1), SC-3000 (1), Schéma (5), Synthèse (15), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), vidéo (4), Z80 (23), z88dk (1), ZX0 (1)

Les derniers articles

Forth sur 6502, épisode 13
Forth sur 6502, épisode 12
Compression de données, la vidéo
TextooM, un jeu pour CP/M
Micreversi, un micro-jeu pour Canon X07
Comparaisons 8 bits en Z80
z80dezasm, désassembler et commenter le Z80
Adressage 6502, un récapitulatif
Forth sur 6502, épisode 11
Deux livres pour le développement sur Famicom

Atom Feed

Réseaux