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.