Site logo

Triceraprog
La programmation depuis le Crétacé

Forth sur 6502, épisode 4 ()

NEXT

Nous y voilà ! Après avoir mis en place un framework de tests dans l'article précédent, il est temps d'implémenter le mot NEXT.

Un petit rappel du premier article de Moving Forth : NEXT est le mot qui permet de faire avancer l'exécution d'une séquence de pointeurs vers des mots Forth. Son pseudo-code (en modèle Indirect Threaded Code) est le suivant :

    (IP) -> W   récupère la mémoire pointée par IP dans le registre "W"
                -> W contient maintenant l'adresse du Code Field
    IP+2 -> IP  avance IP, le compteur de programme
    (W) ->  X   récupère la mémoire pointée par W dans le registre "X"
                -> X contient maintenant l'adresse du code d'exécution
    JP (X)      saute à l'adresse présente dans le registre X

Implémentation

En 6502, le code est un peu long, car l'adressage indirect nécessite de passer par des registres en page zéro. Et les manipuler demande plusieurs instructions. Je ne suis pas très versé en 6502, on verra s'il y a moyen de faire mieux plus tard.

Dans certains Forth, NEXT est tellement court qu'il se pose la question d'en faire une macro. Ici, la réponse est assez simple : c'est non. Le code est assez long, et donc NEXT sera appelé avec un JMP à son adresse.

Très bien, j'ai donc NEXT. Il me faut aussi une liste de mots à appeler. Le but étant d'implémenter l'écriture d'une valeur en mémoire, je vais créer un pseudo-mot TEST-WORD. Puis comme il faut faire cela en boucle, un second mot qui réinitialisera l'IP (Instruction Pointer) au début de la séquence, que je vais appeler RESET-CODE.

Cela donne quelque chose comme ceci :

loop_words:
    .word test_word_cfa
    .word reset_code_cfa

Les pointeurs sont vers les CFA (Code Field Address) des mots. Pour rappel, le CFA est l'adresse où se trouve le code machine qui sera en charge d'exécuter le mot suivant (oui, c'est bien du Indirect Threaded Code, c'est indirect).

Par exemple, pour TEST-WORD, le CFA est l'adresse de la première instruction assembleur qui compose le mot :

test_word_cfa:
    .word test_word_pfa
test_word_pfa:
    lda #$42
    sta $7FF
    jmp next

NEXT va lire le CFA, puis sauter à l'adresse indiquée, qui est test_word_pfa. Et donc exécuter le code qui place la valeur $42 à l'adresse $07FF, avant de redonner la main à NEXT.

J'ai dit plus haut que c'était un pseudo-mot. En effet, le CFA et le PFA ne sont qu'une partie de la définition d'un mot complet en Forth. Mais comme cette partie n'a pas été abordée pour le moment, et qu'elle n'est pas nécessaire pour faire fonctionner NEXT, je laisse ceci de côté.

Reste à bootstrapper l'IP pour qu'il pointe sur loop_words au démarrage du programme. J'ajoute donc un petit code d'initialisation qui... sera aussi le code du mot RESET-CODE :

boot_forth:

    ; Set the Instruction Pointer to the start of temporary Forth code
reset_code:
    lda #<loop_words
    sta REG_IP
    lda #>loop_words
    sta REG_IP + 1

    ; Jump to the Forth NEXT loop
    jmp next

Plus tard, il faudra aussi initialiser d'autres registres dans ce code. Mais pour le moment, ça suffit.

J'enlève l'ancien code d'écriture en mémoire de la boucle principale, je remplace par un appel à boot_forth, et voilà ! Les tests passent... Mais le code d'affichage n'affiche plus le HELLO FAMICOM du squelette. C'est normal puisque la boucle principale Forth n'appelle pas le code de synchro et d'affichage.

La boucle complète

J'ai tous les éléments pour remettre en place la boucle principale. Il suffit de créer deux mots (ou plutôt pseudo-mots) qui feront juste un appel aux deux routines présentes initialement dans la boucle principale.

read_joy_safe_word_cfa:
    .word read_joy_safe_word_pfa
read_joy_safe_word_pfa:
    jsr read_joy_safe
    jmp next

post_logic_word_cfa:
    .word post_logic_word_pfa
post_logic_word_pfa:
    jsr post_logic
    jmp next

Et de modifier la liste des mots à appeler dans loop_words :

loop_words:
    .word read_joy_safe_word_cfa
    .word test_word_cfa
    .word post_logic_word_cfa
    .word reset_code_cfa

Et l'affichage remarche. Avec beaucoup plus d'indirections et donc plus lent, c'est vrai. Je verrai plus tard comment regrouper les appels. Ou comment modifier les sous-routines pour les implémenter directement comme des mots.

En tout cas pour le moment, j'ai retrouvé le comportement initial. Parfait.

DOCOL et ;S

Dans la partie 2 de cette série, j'avais listé les mots DOCOL et ;S comme prioritaires à implémenter. En effet, ces mots permettent d'appeler des mots définis par d'autres mots. C'est exactement ce qui est fait avec loop_words. Sauf que cela n'est pas un vrai mot. Il n'a même pas de CFA. Cette liste est démarrée manuellement par le code d'initialisation, que je rappelle en boucle.

Mais cette boucle est aussi un petit hack avec le RESET-CODE. Je ne peux pas l'utiliser proprement pour démontrer DOCOL et ;S. Je vais donc créer un autre pseudo-mot

main_loop_cfa:
    .word docol
main_loop_pfa:
    .word read_joy_safe_word_cfa
    .word test_word_cfa
    .word post_logic_word_cfa
    .word do_semi_cfa

docol étant le code machine de DOCOL et do_semi_cfa le pointeur sur le CFA de ;S.

La boucle « hack » se retrouve donc remplacée par :

loop_words:
    .word main_loop_cfa
    .word reset_code

Pseudo-code pour les deux mots

Le pseudo-code de DOCOL est le suivant :

   PUSH IP      pousse IP sur la pile de retour
   W+2 -> IP    puisque W pointait sur le Code Field,
                W+2 est l'adresse du Parameter Field.
                Le Parameter Field contient la liste des
                adresses des mots à exécuter. IP pointe
                donc sur cette liste.
   JUMP NEXT    saute à NEXT pour continuer l'exécution

Le pseudo-code de ;S est le suivant :

   POP IP       récupère l'adresse de retour depuis
                la pile de retour et la place dans IP
   JUMP NEXT    saute à NEXT pour continuer l'exécution

Et ça fonctionne

À nouveau, les tests passent et l'affichage fonctionne. Les trois premiers mots Forth sont donc implémentés : NEXT, DOCOL et ;S.

Moving Forth, article 3

Jetons à présent un œil sur le troisième article de la série Moving Forth. Son titre est « Demystifying DOES> », mais il en profite surtout pour décortiquer le fonctionnement des CFA et PFA.

On passe sur l'introduction, qui revient sur des erreurs dans les parties précédentes pour arriver à l'explication du Code Field.

Le Code Field, c'est le cœur du fonctionnement d'un mot en Forth. C'est un indicateur de la nature du mot, de la façon dont il va se comporter, être exécuté. Le Code Field indique quoi faire avec les paramètres du mot, les paramètres étant situés dans le Parameter Field, qui suit le Code Field.

Ainsi, on peut définir différents fonctionnements :

  • un mot qui exécute les paramètres comme du code machine,
  • un mot qui traite les paramètres comme des référence à d'autres mots,
  • un mot qui traite les paramètres comme un emplacement mémoire réservé pour une variable,
  • etc...

En quelque sorte, le Code Field représente une routine avec un paramètre implicite : le Parameter Field.

L'article fait un parallèle avec la programmation objet, où le Code Field serait la méthode unique d'une classe, et le Parameter Field l'instance de cette classe pour chaque mot utilisant ce Code Field. Je ne suis pas certain que ce parallèle aide beaucoup... mais pourquoi pas.

Code Field et Indirect Threaded Code

Dans le modèle ITC (Indirect Threaded Code), le Code Field est une adresse qui pointe vers un code machine chargé d'interpréter le contenu du Parameter Field. On a vu deux cas pour le moment :

  • un Code Field qui pointe vers DOCOL entraîne l'appel successif de tous les mots dont les listés dans le PFA. Ces mots sont eux-mêmes représentés par leur CFA.
  • un Code Field qui pointe vers directement sur l'adresse du Parameter Field. Dans ce cas, le code machine est directement dans le PFA.

Il existe différents Code Field standards en Forth, mais rien n'empêche l'utilisateur d'en définir de nouveaux. Et c'est exactement ce que fait le mot DOES> mentionné par le titre. Cependant, son fonctionnement nécessitera probablement un article à lui seul. Retenez juste que DOES> permet de définir une nouvelle classe de mots partageant un même Code Field personnalisé.

L'article mentionne aussi les mots CREATE ou encore ;CODE, qui respectivement permettent de définir un nouveau mot et de définir un mot dont le code est écrit en assembleur. Là encore, ce sont des sujets pour plus tard lorsqu'il sera question de définir des mots par programmation.

Pour le moment, je n'ai toujours que des pseudo-mots, et ils sont tous définis directement dans le code assembleur.

L'article est plus long car il aborde ces sujets dans les différents modèles d'exécution de Forth, ainsi que les mots CONSTANT et VARIABLE, qui sont des mots de constructions avec un Code Field particulier, que nous verrons plus tard.

La suite

J'ai à présent les premiers pseudo-mots pour mon Forth, mais il m'en manque un pour créer la boucle en entier. En effet, pour le moment je réinitialise l'interpréteur pour relancer la boucle. Ce n'est pas une vraie boucle Forth. Je pense que ma prochaine étape sera donc de créer le mot BRANCH, qui modifie le registre IP.