Site logo

Triceraprog
La programmation depuis le Crétacé

  • Forth sur 6502, épisode 16 ()

    La création interactive

    Forth n'est pas seulement un langage, c'est aussi (et avant tout ?) un environnement interactif de développement. Il est possible dans une session de compiler des mots additionnels au dictionnaire. Le mot principal pour opérer est :.

    Par exemple :

    : AJOUTE_2 2 +
    

    Cela définit un mot qui ajoute 2 à la valeur au sommet de la pile.

    100 AJOUTE_2 .
    

    ... affiche donc 102 à l'écran (le mot . affiche le contenu du sommet de la pile en tant que nombre).

    Structure d'un mot

    Il avait déjà été question de la structure d'un mot dans un Forth 8 bits classique lors de la partie 6 de ces articles. Ça remonte un peu. On peut aussi trouver des éléments dans la documentation de Pampuk Forth que j'avais écrite il y a un moment.

    Rapidement, un mot est composé d'un NFA (longueur du nom et drapeaux, ainsi que le nom), d'un LFA (lien vers le mot précédent dans le dictionnaire), d'un CFA (adresse du code assembleur à exécuter pour ce mot) et d'un PFA (les paramètres).

    Chaque nouveau mot se place à la suite des précédents, donc à l'emplacement désigné par HERE. Lorsque l'on sait qu'il existe un mot C, qui place la valeur au sommet de la pile en HERE en tant qu'octet et avance HERE (, fait la même chose pour une cellule complète), on peut donc tout à fait compiler un nouveau mot « à la main » :

    HEX         \ passage en hexadécimal
    HERE        \ garder la valeur de HERE sur la pile pour plus tard
    03 C,       \ longueur du mot
    4D C,       \ M
    4F C,       \ O
    D4 C,       \ T (54) et bit 7 (80) du dernier caractère du mot à 1
    LATEST ,    \ LFA : lien vers le dernier mot défini du dictionnaire
    ' DOVAR CFA ,   \ CFA : compile le CFA de DOVAR (qui met sur la pile le contenu de la première cellule du PFA)
    0000        \ PFA : le contenu de la variable... 0
    CURRENT @ ! \ met à jour le dictionnaire en y incluant ce mot
                \ grâce à l'adresse HERE mise sur la pile au début
    

    Voilà donc comporte comme une variable. Il manque des mots actuellement pour que cela fonctionne (DOVAR n'est pas accessible directement, ', C, et , non plus, CURRENT concerne la gestion du dictionnaire, que j'ai mis de côté pour le moment).

    Imaginez faire ça pour chaque nouveau mot... Forth propose donc des mots pour répéter ce procédé. Et comme tout en Forth, il n'y a pas de procédé magique, créer un nouveau mot se fait avec d'autres mots.

    Création de l'entête

    C'est le mot CREATE qui se charge de construire un mot. Son implémentation commence de la même manière que FIND, en allant chercher le nom d'un mot dans le flux d'entrée pour le normaliser et le mettre à HERE.

    : CREATE
        BL WORD             \ récupération du mot
        HERE COUNT CAPITAL  \ passage en majuscules
        (HEADER)            \ création de l'entête
        ;
    

    (HEADER) est un mot écrit en assembleur qui met en place l'en-tête. C'est assez simple car WORD a posé une chaîne préfixée par sa longueur à HERE, c'est-à-dire pile à l'endroit où sera la NFA du nouveau mot. C'est bien fait non ?

    Il faut donc mettre le bit 7 du dernier caractère du nom du mot à 1 (merci l'indexage par Y), ajouter le flag SMUDGE à la longueur puisque le mot n'est pas encore validé (voir paragraphe suivant), et mettre la valeur de LATEST au LFA. Le CFA est celui de DOVAR, qui exécute une variable, car c'est un mot sûr : il pousse sur la pile sa propre adresse PFA. Pas de danger.

    Enfin, HERE est placé au PFA et le dictionnaire est mis à jour. Comme c'est un mot assembleur interne, on peut directement manipuler la valeur de LATEST, qui devrait sinon passer par CURRENT, dont on ne dispose pas.

    Tout est prêt pour compléter le mot ! Il faut ajouter le vrai contenu du PFA et changer le CFA pour le type de mot que l'on veut, en mettant à jour HERE. Avec , et C, par exemple, mais aussi pourquoi pas, avec ALLOT.

    ALLOT ? Je n'en avais pas parlé mais il est logique de l'implémenter maintenant, même s'il n'est pas encore utilisé. C'est un mot qui réserve de la place en mémoire. La place en mémoire réservable en Forth est à HERE. Ce que fait ALLOT est donc juste ajouter à HERE le nombre d'octets que l'on veut réserver.

    10 ALLOT    \ réserve 10 octets, autrement dit, ajoute 10 à HERE
    

    Les drapeaux du NFA

    Il y a deux drapeaux qui sont mêlés à la longueur dans le NFA d'un mot, SMUDGE et IMMEDIATE, qui sont aussi les noms des mots qui permettent de manipuler ces drapeaux.

    SMUDGE est placé lors de la création d'un mot, pour éviter que FIND le trouve alors que celui-ci n'est pas complet. C'est en quelque sorte un panneau indiquant en construction. Lorsqu'un mot est complètement terminé, le drapeau SMUDGE est enlevé grâce au mot du même nom (qui est en fait une bascule, SMUDGE SMUDGE revient à ne rien faire).

    Bien entendu, il a fallu adapter FIND (ou plus exactement (FIND)) pour que l'algorithme saute par-dessus les mots dont le drapeau SMUDGE est à 1.

    IMMEDIATE est un autre drapeau qui indique que le mot est immédiat... On verra cela lorsqu'on reviendra sur INTERPRET. Mais brièvement voici le principe : lorsqu'on est en train de compiler un mot, INTERPRET se contente de compiler les CFA des mots, ainsi que les nombres, à la suite de HERE. Un mot immédiat est une exception qui dit : ce mot-là ne doit pas être compilé, mais exécuté, même si on est en mode compilation.

    Cela permet de travailler sur la compilation elle-même. Et le premier usage est assez important : pouvoir sortir du mode de compilation !

    Le mot IMMEDIATE est aussi une bascule, et tout comme SMUDGE agit sur le dernier mot qui a été défini dans le dictionnaire (LATEST).

    Interprétation de la compilation

    Comme indiqué ci-dessus mais aussi dans les articles précédents concernant INTERPRET, le mot a deux comportements différents suivant s'il est en mode exécution (le mode implémenté pour le moment) ou en mode compilation (le mode à ajouter pour pouvoir implémenter :).

    Rappel d'INTERPRET avec juste la partie exécution :

    : INTERPRET
      BEGIN
        -FIND IF
          DROP            \ oublie la longueur du nom retourné par -FIND
          CFA EXECUTE
        ELSE
          HERE
          C@ 0= IF EXIT THEN  \ fin de la boucle, quitte le mot
          HERE NUMBER         \ convertit en nombre
        ENDIF
      AGAIN ;
    

    Voici le code incluant la partie compilation :

    : INTERPRET
      BEGIN
        -FIND IF    \ ça ne change pas, on commence par chercher le mot à interpréter
          STATE @   \ et on vérifie le mode actuel. Si c'est 128, on est en mode
                    \ compilation. 0, en mode exécution
          U<        \ si la longueur/drapeau est inférieur à STATE en non signé,
                    \ c'est que STATE était égal à 128 et le drapeau IMMEDIATE n'était
                    \ pas placé
          IF        \ c'est donc une compilation
            CFA     \ récupération du CFA depuis le PFA retourné par -FIND
            ,       \ compilation du CFA (c'est-à-dire, ajoute le CFA à HERE, avec
                    \ HERE normalement dans le PFA d'un mot en cours de création)
          ELSE
            CFA EXECUTE     \ exécution du mot, qu'on soit en mode de compilation
                            \ ou bien que ce soit un mot immédiat
          ENDIF
        ELSE
          HERE
          C@ 0= IF EXIT THEN    \ fin de la boucle, quitte le mot
          HERE NUMBER           \ convertit en nombre
          LITERAL               \ voir ci-dessous
        ENDIF
    
      AGAIN ;
    

    Comme on peut le voir, le changement est globalement de vérifier STATE et savoir si le mot est immédiat ou pas. Si cela indique une compilation, le CFA est compilé dans le PFA du mot. Attention, comme souvent, Forth part du principe que tout s'enchaîne logiquement. Il vaut mieux être en train de compiler un mot, sinon, on ajoute des CFA dans HERE sans effet (ce qui n'est pas non plus très grave, ça fait juste perdre de la place).

    Pour la partie nombre, un nouveau mot s'occupe de tout : LITERAL. En mode exécution, ce mot ne fait rien et laisse le nombre sur la pile. En mode compilation, le mot compile un LIT suivi du nombre trouvé sur la pile, compilant de fait la mise sur la pile d'un nombre, lors de l'exécution future.

    Pour implémenter INTERPRET, il manque U<, une comparaison non signée, et STATE, une variable qui est initialisée lors du reset de l'environnement (dans COLD). J'en ai profité pour implémenter quelques mots de comparaison (<, >, 0>).

    LITERAL est le genre de mots Forth pour lesquels un peu de gymnastique est nécessaire :

    : LITERAL
      STATE @ IF    \ compilation ?
      IF
        LIT ' LIT   \ met le CFA de LIT sur la pile (grâce au premier LIT)
        ,           \ compile le CFA de LIT
        ,           \ compile le nombre
      ENDIF
      ;
    

    Deux-points et point virgule

    C'est bon, j'ai tout pour implémenter les deux mots centraux dans la création de mots Forth de manière interactive à partir d'autres mots Forth.

    Quelles sont les étapes ?

    1. Créer un mot. Pour ça, on a CREATE.
    2. Changer le CFA du mot pour que ce soit DOCOL (l'interpréteur de mots Forth).
    3. Passer en mode compilation, pour que INTERPRET compile les mots qui suivent.
    4. Compiler ;S en dernier mot.
    5. Valider le mot avec SMUDGE.
    6. Sortir du mode compilation.

    Les étapes 1, 2 et 3 sont la responsabilité de :. Les étapes 4, 5 et 6 celles de ;.

    Voilà à quoi cela ressemble :

    HEX
    : :         \ oui, bon, définir : avec lui-même ne fonctionne pas
                \ mais c'est une manière de l'écrire
      CREATE    \ création du mot
      LIT DOCOL \ adresse de DOCOL (voir note ci-dessous)
      HERE CFA  \ le CFA du mot actuel (qui est actuellement sur le PFA)
                \ pourrait aussi s'écrire LATEST PFA CFA
      !         \ patch du CFA du mot qui vient d'être créé
      80 STATE !\ passe en mode compilation
      ;         \ bon... là aussi, on ne l'a pas encore défini...
    

    Deux commentaires. Le premier est que ce n'est pas l'implémentation classique pour patcher le CFA du mot qui vient d'être créé. L'implémentation classique est d'utiliser ;CODE, qui est l'équivalent de ; pour les mots créés par CODE. Le mot patch le CFA pour le faire pointer vers l'adresse qui suit le mot, qui doit contenir du code en assembleur. L'astuce est alors de faire suivre : par le code de DOCOL, et hop... Je réimplémenterai peut-être ça de cette manière, mais pour l'explication, c'est plus simple de détailler comme ci-dessus.

    Autre commentaire : 80 STATE ! a un nom, c'est le mot ]. Le mot [, lui, passe en mode interprétation en faisant 0 STATE !. Et ce sont deux mots immédiats.

    Cela permet, lors de la compilation d'un mot par :, de sortir temporairement du mode compilation pour effectuer des opérations. Cela encadre élégamment le code interprété entre [ et ], ce qui se comprend bien visuellement.

    Et pour ;

    : ;
      ' ;S ,        \ voir note ci-dessous
      SMUDGE        \ le mot devient visible
      LIT 0 STATE ! \ voir le commentaire ci-dessus !
      ;             \ oui oui... certes certes
      IMMEDIATE     \ le mot doit être immédiat, sinon, il serait compilé
                    \ dans le mot en train d'être défini
    

    ' ;S , se lit : prendre le CFA de ;S et le compiler. C'est ce que l'on veut. Mais il y a un mot pour ça : COMPILE. Ce mot prend le mot qui suit (dans un mot compilé), prend son CFA et le compile.

    Avec toutes ces notes, cela donne au final :

    : : CREATE LIT DOCOL HERE CFA ! ] ;
    : ; COMPILE ;S [ ; IMMEDIATE
    

    Création de mots

    Enfin, la création de mots est possible. On peut écrire, par exemple : DOUBLE DUP + ; qui définit un mot qui double le nombre sur le haut de la pile.

    Mais il n'y a toujours pas de mots pour afficher les nombres.

    Toute cette suite de définition des mots standards n'est pas très intéressante. Je vais prendre leurs implémentations et les traduire en mode « ROM ». La plupart sont des transcriptions directes, seuls ceux avec des branchements doivent être un peu adaptés. Je reviendrai avec un article lorsqu'il y aura du nouveau intéressant.

    Forth sur 6502 - définition d'un mot et utilisation


  • Forth sur 6502, épisode 15 ()

    Le bout

    Dans la partie 12 de cette série d'articles, j'annonçais m'attaquer à l'interprétation d'une ligne de Forth. Dans la partie 15... j'en vois le bout !

    Forcément, avec la boucle QUIT, le mot INTERPRET forme le moteur de la partie interactive de Forth. Dans l'article précédent, j'avais énuméré tous les mots nécessaires à l'implémentation de NUMBER, élément manquant pour INTERPRET en mode d'interprétation (sans compilation). Et je terminais par si tout s'aligne parfaitement...

    ... quelques heures plus tard, en mettant au point NUMBER, je m'aperçois que je me suis trompé dans l'implémentation de DIGIT. Je n'avais pas écrit la signature en commentaire, codant de mémoire... et oubliant donc que la BASE qu'utilise DIGIT est un paramètre sur la pile, et non la variable directement. Ce fut vite corrigé.

    J'ai aussi découvert que la chaîne du dictionnaire était cassée et que CFA était laissé de côté. Ce n'est pas la première fois que ça m'arrive, ni la première fois que je devrais trouver un test pour m'avertir, mais je ne l'ai toujours pas fait... Comme dans les tests je trouve les mots à appeler non par -FIND mais par leurs symboles assembleurs en direct, le fait qu'ils ne soient pas visibles dans le dictionnaire n'est pas détecté.

    Et le test qui vérifie l'intégrité de la chaîne ne fait que cela, le test n'a pas la liste des mots qui devraient être trouvés, je trouvais ça un peu lourd à maintenir.

    INTERPRET ! INTERPRET !

    Tout cela fait, il est donc temps, ouf, ça y est, d'implémenter INTERPRET.

    Le code (que je retravaille au fur et à mesure de l'implémentation réelle des mots) est actuellement celui-ci :

    : INTERPRET
      BEGIN
        -FIND IF
          DROP            \ oublie la longueur du nom retourné par -FIND
          CFA EXECUTE
        ELSE
          HERE NUMBER     \ convertit en nombre
        ENDIF
      AGAIN ;
    

    Ce code a un souci cependant... enfin deux. D'abord, il ne s'arrête pas à la fin du buffer. J'avais décidé dans le précédent article que -FIND laisserait une chaîne nulle dans HERE, mais je ne le teste pas encore.

    Voici ce que cela peut donner :

    : INTERPRET
      BEGIN
        -FIND IF
          DROP            \ oublie la longueur du nom retourné par -FIND
          CFA EXECUTE
        ELSE
          HERE
          C@ 0= IF EXIT THEN  \ fin de la boucle, quitte le mot
          HERE NUMBER         \ convertit en nombre
        ENDIF
      AGAIN ;
    

    Les mots EXIT et 0= n'existent pas pour le moment, mais étant donné que le mot est implémenté directement par une suite de CFA, 0BRANCH suffit.

    Ni mot, ni nombre

    Ensuite, il reste un dernier problème à régler : pour le moment, si -FIND ne trouve pas le mot dans le dictionnaire, alors c'est forcément un nombre ou la fin du buffer. Mais il est possible que le mot ne soit ni dans le dictionnaire, ni la représentation d'un nombre. Et dans ce cas, NUMBER pousse 0 sur la pile des paramètres. Ce n'est pas ce que l'on veut.

    Pour détecter que l'on essaye de convertir en nombre une chaîne qui n'en est pas une représentation, voici ce que l'on trouve dans des sources :

    • (NUMBER) s'arrête au premier signe qui n'est pas considéré comme un chiffre valide et l'adresse mise à jour de la chaîne analysée se retrouve alors sur la pile.
    • On peut ajouter à WORD le contrat suivant : il y a toujours un espace après le mot placé à HERE.
    • Si au retour de (NUMBER), NUMBER détecte autre chose qu'un espace à l'adresse retournée, alors une erreur est lancée, ce qui arrête l'interprétation.
    • Il faut ajouter un mot ERROR qui provoque une erreur. Une erreur affiche le contenu présent à HERE suivi d'un ? puis branche sur ABORT, qui vide les piles (reset l'environnement plus généralement) et relance la boucle QUIT.

    Le code Forth de ERROR est celui-ci :

    : ERROR
        HERE COUNT TYPE     \ affiche le dernier mot
                            \ que WORD a mis en HERE
        ."  ?"              \ il n'y a pas de support de chaîne
                            \ pour le moment mais ça ressemblerait à ça
        ABORT
        ;                   \ techniquement, il faut juste sortir du mode
                            \ compilation et rendre actif le mot. Mais
                            \ on n'a pas vu ça pour
                            \ le moment, donc je me contente d'un ;
    

    Ça fonctionne !

    J'ai dû corriger deux ou trois bugs sur le chemin (comme ACCEPT qui ne poussait pas la longueur du mot correctement sur la pile), mais voilà, ça fonctionne ! Je peux à présent simuler l'appui de touches depuis le harnais de test et vérifier le fonctionnement de l'interprétation à l'écran.

    Je n'ai pas encore ., donc je ne peux pas afficher de résultat numérique, par contre, j'ai EMIT, et en ajoutant CR, je peux faire CR 65 EMIT 66 EMIT CR !

    Interprétation interactive sur Family Forth

    Et le test mémoire ?

    Oui, ça y est, il est temps ! Il est temps d'utiliser un test avec écriture au clavier de HEX FF 7FF C! afin de capturer le changement mémoire depuis le harnais de tests et d'enlever le code en dur d'écriture mémoire qui existe depuis pratiquement le début de l'écriture de ce Forth.

    MEMORY_TEST, TEST_VALUE et TEST_ADDR peuvent être supprimés du code et le test de Memory Watch remplacé par un test de Memory Watch à partir d'un test d'INTERPRET.

    Pfiuu ! Quelle aventure !

    Il est temps aussi de faire une petite pause de réflexion pour, déjà, passer à l'étape suivante : la création de mots, et donc la partie compilation d'INTERPRET.


  • Forth sur 6502, épisode 14 ()

    Forth et les nombres

    À sa création, Forth ne manipule que les nombres entiers. C'est suffisant pour son usage. Et un nombre se stocke dans une cellule de données, généralement (et dans le choix d'implémentation de ce Forth sur 6502) une cellule de 16 bits pour un Forth sur processeur 8 bits.

    Par la suite, les nombres « double » seront ajoutés, des nombres stockés sur deux cellules donc. Mais là où la plupart des langages de haut niveau utilisent les mêmes symboles arithmétiques quel que soit le type de donnée numérique, voire même font de la coercition (adapte les types de données pour pouvoir les manipuler ensemble), Forth n'a rien de tout cela : une addition de deux nombres « double » n'utilise pas le même symbole que l'addition de deux nombres simples.

    Par exemple

    4 6 + .      \ fait la somme de deux nombres simples (et l'affiche)
    4. 6. D+ D.  \ fait la somme de deux nombres doubles (et l'affiche)
    

    Le point dans un nombre indique un nombre double. Ce point peut d'ailleurs être n'importe où, ce qui est une bizarrerie pour des habitués de langages plus modernes. Ainsi 40.00, 4.000 ou 4000. représentent tous le nombre double 4000... Troublant.

    De même pour les implémentations de nombres flottants, qui sont d'ailleurs souvent dans les implémentations historiques soit inexistants, soit sous forme d'ajout optionnel.

    Pour cette implémentation sur 6502 pour Famicom, je vais me contenter, au moins dans un premier temps, de traiter les nombres simples. Il sera toujours possible d'ajouter les nombres doubles si besoin, mais... le besoin ne me semble pas évident pour le moment.

    Une question de bases

    Dans le monde des ordinateurs, depuis un bon moment, c'est la base 2 qui fait référence. Un nombre stocké dans une machine est fait de 0 et de 1, si vous êtes en train de lire cet article, je ne dois certainement rien vous apprendre.

    Je ne vous apprends pas grand chose de plus en vous disant que de manière habituelle, les humains contemporains dans leur grande majorité comptent en base 10, avec des chiffres allant de 0 à 9.

    Forth est capable d'analyser un nombre sous forme de chaîne de caractères en n'importe quelle base allant de 0 à 16. Et même d'écrire ces nombres à l'écran. En interne, ces nombres sont bien entendu en binaire et, on l'a vu plus haut, sont stockés naturellement dans une cellule de données, 16 bits pour cette implémentation.

    Il est donc nécessaire d'avoir un mot Forth qui prend une chaîne de caractères représentant un nombre et qui la transforme en ce même nombre en base 2. Ainsi que l'inverse, prendre un nombre interne en base 2 et le transformer en suite de caractères représentant ce même nombre dans une base donnée.

    Le premier mot est NUMBER, le second est . (point). Techniquement, point fait trop de choses : il affiche un nombre sur l'écran, il ne fait pas que la conversion, mais on va le garder en tête pour le moment. Et uniquement en tête car l'objectif courant est toujours le même, interpréter la ligne HEX FF 7FF C! afin de valider l'interpréteur grâce à un point d'arrêt dans le harnais de test. Et . n'est pas nécessaire pour cela.

    HEX est l'équivalent de 16 BASE !, en supposant que la base actuelle est décimale. De manière similaire DECIMAL est l'équivalent de A BASE !, en supposant que la base actuelle est hexadécimale. La raison première de l'existence de ces deux mots est d'ailleurs, à mon avis, de ne pas se soucier de la base actuelle pour alterner entre les deux bases les plus usuelles.

    Il nous faudra donc une variable BASE, ce qui est trivial.

    D'une chaîne vers du binaire

    Reste donc le vrai gros morceau. En Fig-Forth, NUMBER a pour signature ( addr - d ), c'est-à-dire qu'à partir d'un pointeur vers une chaîne précédée de sa longueur, sa représentation en nombre « double » est poussée sur la pile. Cependant, comme cette implémentation n'a pas de nombre double, je choisis pour signature ( addr - n ).

    C'est un choix assez fort : une implémentation des nombres doubles dans le futur nécessitera une adaptation de tous les appels à NUMBER. Mais vraiment, je ne vois pas ce futur dans ce Forth pour Famicom que j'imagine (famous last words ?).

    NUMBER est généralement un mot en Forth qui utilise (NUMBER) mais aussi plein de mots que je n'ai pas encore implémentés, comme DIGIT. C'est un mot qui ne sert qu'à la conversion des nombres dans un code source, donc sans besoin critique de performance. Voilà un source adapté aux seuls nombres simples :

    : NUMBER      \ ( addr - n )
      0           \ accumulateur pour le résultat
      SWAP        \ pointeur de chaîne (addr) au-dessus de la pile
      DUP 1+ C@   \ prend le premier caractère de la chaîne
      2D = DUP >R \ regarde s'il est égal au signe - (moins) et duplique le résultat sur la pile de retour
      +           \ s'il y avait un signe moins, le pointeur de chaîne est avancé d'un (1)
    
      (NUMBER)    \ appel de (NUMBER) avec la chaîne ajustée
      DROP        \ ignore l'adresse de retour de (NUMBER) (2)
    
      R>          \ récupère l'existence d'un signe moins depuis la pile de retour
      IF
        MINUS     \ oppose le signe du nombre s'il y avait un signe moins en début de chaîne
      ENDIF
      ;
    

    (1) Ce recalage peut sembler étrange, car il place le pointeur de la chaîne à analyser non pas sur le premier caractère mais une adresse avant. Puis (NUMBER), juste après, commence avant toute chose à avancer le pointeur d'une position. Pourquoi ne pas l'y positionner tout de suite ?

    Cela permet à (NUMBER) de fonctionner avec des chaînes préfixées par la taille. (NUMBER), contrairement à NUMBER, ne fait pas de traitement de signe négatif, ni, dans le cas où cela est implémenté, de traitement de nombre double... ou presque.

    (2) Dans l'implémentation avec les nombres doubles, l'adresse permet de vérifier si un . (point) est présent, et si c'est le cas, boucler pour traiter la deuxième partie du nombre. Ici, l'adresse ne nous intéresse pas.

    Toute cette partie a été simplifiée ici, mais par curiosité, allez voir une implémentation complète, c'est étonnant.

    : (NUMBER)  \ ( n1 addr1 - n2 addr2 )
      BEGIN
        1+      \ avance de 1 le pointeur de chaîne
        DUP >R  \ copie cette adresse sur la pile de retour (1)
    
        C@      \ met le caractère pointé sur la pile
        BASE @  \ récupère la base courante
        DIGIT   \ transforme ce chiffre alphanumérique en représentation numérique
        \ ici, la pile contient 1 (VRAI) ou 0 (FAUX) suivi 
        \ du le chiffre en représentation machine dans le cas VRAI
        \ DIGIT a consommé le caractère et la BASE
    
      WHILE      \ si DIGIT a renvoyé VRAI (sinon, va au REPEAT)
        SWAP     \ récupère l'accumulateur
        BASE @   \ ainsi que la base
        U* DROP  \ pour les multiplier (2)
        +        \ ajoute le résultat à l'accumulateur
        R>       \ récupération de l'adresse de chaîne avant nouvelle boucle
      REPEAT
      R>         \ remise sur la pile de l'adresse de chaîne
                 \ après son analyse (pointe donc après le nombre)
      ;
    

    (1) la pile de retour sert parfois, dans certains mots, comme pile temporaire supplémentaire à la pile des paramètres. Cela arrive lorsqu'il y a trop de paramètres à gérer sur la pile des paramètres. Il est bien entendu indispensable que le mot remette tout en place avant de se terminer, puisque les informations pour revenir au mot appelant sont présentes dans la pile de retour.

    (2) U* renvoie un nombre double à partir de la multiplication de deux nombres simples ( n1 n2 - d ). Il faut donc oublier la partie haute avec un DROP. U* fait spécifiquement une multiplication non signée, contrairement à *. Je garde la signature et la sortie avec un nombre double même si l'implémentation ne le supporte pas plus, car la routine de multiplication fait de toute façon le calcul, et pour éviter de surprendre avec une signature non usuelle.

    Les mots manquants

    Pour implémenter NUMBER et (NUMBER), je vais avoir besoin des mots suivants :

    Des mots de manipulation simples :

    • SWAP ( n1 n2 - n2 n1 ) : inverse les positions des deux éléments les plus en haut de la pile.
    • DUP ( n - n n ) : duplique le mot au sommet de la pile.
    • C@ ( addr - n ) : met sur le sommet de la pile l'octet présent à l'adresse donnée (pour rappel, @ (fetch) met sur le sommet de la pile la cellule présente à l'adresse donnée, donc deux octets).

    Un peu d'arithmétique et de logique :

    • 1+ ( - ) : ajoute 1 à la valeur présente au sommet de la pile.
    • U* ( n1 n2 - d ) : voir plus haut, effectue une multiplication non signée, avec en résultat un nombre double.
    • MINUS ( n1 - n2 ) : place sur la pile l'opposé du nombre qui y était présent.
    • + ( n1 n2 - n3 ) : effectue l'addition de deux nombres.
    • = ( n1 n2 - b ) : compare deux nombres et place 1 (vrai) sur la pile s'ils sont égaux, 0 sinon.

    De la manipulation de pile de retour :

    • >R ( n - ) : envoie le nombre au sommet de la pile vers la pile des retours.
    • R> ( - n ): récupère depuis la pile des retours la valeur au sommet et la pousse sur la pile des paramètres.

    Pour le besoin des tests, j'ajoute R, qui copie le haut de la pile des retours sur la pile des paramètres. Cela permet de vérifier avec >R R R> que la valeur sur la pile des paramètres a été dupliquée. C'est un DUP en plus complexe. Mais comme >R et R> touchent à la pile des retours, et donc au noyau de l'interpréteur, j'ai trouvé ça moins risqué pour les tests.

    Une variable :

    • BASE : variable qui contient la base numérique actuelle.

    Et enfin DIGIT ( c n1 - n2 b ) : qui convertit le caractère c selon la base n1 en nombre n2 si cela est possible. Dans ce cas, b est égal à 1, dans le cas contraire, b est égal à 0 et il n'y a rien d'autre de mis sur la pile. Et puisque ça ne coûte pas plus cher, la base supportée ira jusqu'à la base 36 (avec toutes les lettres de l'alphabet).

    Tous ces mots seront implémentés en assembleur, puis NUMBER et (NUMBER) seront implémentés en mots Forth.

    Pour passer le temps

    Il n'y a pas beaucoup de choses à montrer dans ces deux derniers articles. C'est beaucoup d'implémentation de fond. Alors en attendant, voici ce à quoi ressemble la sortie du framework de tests après implémentation des tests mais avant l'implémentation des mots du bloc arithmétique et logique :

    Failed tests:
      - 1+ increments TOS: Cannot resolve CFA for word: 1+
      - = returns 0 on inequality: Cannot resolve CFA for word: =
      - + adds two numbers: Cannot resolve CFA for word: +
      - MINUS opposes the TOS: Cannot resolve CFA for word: MINUS
      - U* unsigned-multiplies: Cannot resolve CFA for word: U*
      - = returns 1 on equality: Cannot resolve CFA for word: =
    

    Quelle aventure...

    Tout est implémenté et prêt pour NUMBER, cela sera pour la prochaine fois. Qui sait ? Si tout s'aligne parfaitement, peut-être que INTERPRET suivra dans la foulée !


  • 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 FF 7FF 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 FF 7FF 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.


Page 1 / 29 (suivant) »

Tous les tags

3d (15), 6502 (18), 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 (14), Forth (19), 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 (24), 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 16
Forth sur 6502, épisode 15
Forth sur 6502, épisode 14
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

Atom Feed

Réseaux