Réaliser un interpréteur en Pascal

Avec Free Pascal sous Lazarus

Auteur

Gilles Vasseur

Ce tutoriel vous permet d'apprendre les bases de la programmation avec un dérivé de LOGO ou de vous perfectionner en bâtissant l'interpréteur de A à Z…

Commentez

- Titre : Réaliser un interpréteur en Pascal

- Auteur : Gilles Vasseur

- Parution : 15 mars 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Gilles Vasseur. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Le projet

- II. Les objets de GVLOGO

- III. Récréation : EasyTurtle (logiciel de dessin)

- IV. Les outils de programmation

- V. Les composants du langage

- VI. L'interpréteur

- VI-A. Principe de fonctionnement

- VI-B. Au cœur de l'interpréteur

- VI-B-1. Les structures utiles

- VI-B-2. DoBegin

- VI-B-3. Process

- VI-B-4. DoEnd

- VI-B-5. DoWord

- VI-B-6. DoList

- VI-B-7. DoVar

- VI-B-8. DoNumber

- VI-B-9. DoEval

- VI-B-10. PushConst

- VI-B-11. DoCommand

- VI-B-12. DoPrim

- VI-B-13. DoProc

- VI-B-14. ExeCommand

- VI-B-15. ExePrim

- VI-B-16. DoExePrim

- VI-B-17. ExeProc

- VI-C. Les primitives

- VI-C-1. Utilitaires

- VI-C-2. La classe TGVAutomatMessage

- VI-C-3. Deux primitives ordinaires… et une troisième un peu moins

- VI-C-4. Le programme de test de l'unité GVAutomat

I - Le projet

II - Les objets de GVLOGO

III - Récréation : EasyTurtle (logiciel de dessin)

IV - Les outils de programmation

V - Les composants du langage

VI - L'interpréteur

VI-A - Principe de fonctionnement

L'objectif de l'interpréteur est de partir d'une ligne écrite conformément aux règles du langage GVLOGO et d'exécuter les ordres qu'elle contient.

Il exige :

- un texte en entrée à découper en unités lexicales ;

- une boucle qui parcourt une à une ces unités pour exécuter le code en rapport avec elles ;

- une vérification de son état de sortie.

Le cœur de l'interpréteur est par conséquent essentiellement une boucle.

Les supports de son travail sont des piles et une queue :

- le texte à interpréter est placé sous forme d'unités lexicales dans la queue (Q1), autrement dit dans une file d'attente qui est lue jusqu'à épuisement ou jusqu'à la survenue d'une erreur ;

- une pile des données recueille les constantes (PD) ;

- une pile des commandes (PC) recueille les identificateurs des primitives et des procédures ;

- une pile des paramètres (PP) recueille le nombre de paramètres attendus pour chaque commande rencontrée ;

- une pile d'exécution (PE) recueille les données lorsqu'elles sont en nombre suffisant pour qu'une commande soit exécutée.

C'est le premier caractère de chaque unité lexicale qui permet de savoir quel code doit être exécuté :

- " indique une constante de mot ;

- [ indique une liste ;

- : indique une variable ;

- + - 0..9 indiquent un nombre ;

- a..z A..Z indiquent une commande.

Afin de faciliter la compréhension de l'interpréteur, on suppose qu'on ne doit interpréter que des primitives et des constantes numériques. On néglige le traitement des éventuelles erreurs.

Soit donc la ligne de commandes GVLOGO à interpréter :

ECRIS SOMME 4 5

L'interpréteur place tout d'abord cette ligne dans la file d'attente Q1 :

Q1

PD

PC

PP

PE

5

4

SOMME

ECRIS

La boucle d'interprétation commence alors.

Le premier élément dans la queue est ECRIS.

Traitement de ECRIS.

- ECRIS commence par une lettre.

- C'est donc une primitive.

- On cherche combien de paramètres elle attend (appel au noyau).

- La réponse est 1.

- On empile ECRIS sur la pile PC.

- On empile 1 sur la pile PP.

- Comme le sommet de PP n'est pas nul, on en a fini avec cette unité.

Q1

PD

PC

PP

PE

5

ECRIS

1

4

SOMME

L'élément suivant dans la queue est SOMME.

Traitement de SOMME.

- SOMME commence par une lettre.

- C'est donc une primitive.

- On cherche combien de paramètres elle attend.

- La réponse est 2.

- On empile SOMME sur la pile PC.

- On empile 2 sur la pile PP.

- Comme le sommet de PP n'est pas nul, on en a fini avec cette unité.

Q1

PD

PC

PP

PE

5

ECRIS

1

4

SOMME

2

L'élément suivant dans la queue est 4.

Traitement de 4.

- 4 commence par un chiffre.

- C'est donc un nombre.

- On empile 4 sur la pile PD.

- Une donnée ayant été trouvée, on décrémente le sommet de la pile PP qui vaut à présent 1.

- Comme le sommet de PP n'est pas nul, on en a fini avec cette unité.

Q1

PD

PC

PP

PE

5

4

ECRIS

1

SOMME

1

L'élément suivant dans la queue est 5.

Traitement de 5.

- 5 commence par un chiffre.

- C'est donc un nombre.

- On empile 5 sur la pile PD.

- Une donnée ayant été trouvée, on décrémente le sommet de la pile PP qui vaut à présent 0.

Q1

PD

PC

PP

PE

4

ECRIS

1

5

SOMME

0

- Comme le sommet de la pile vaut 0, on dépile ce 0 inutile. On sait à présent qu'on a assez de paramètres pour la primitive au sommet de PC : on transfère donc le nombre de paramètres attendus de PD vers PE, ce qui permet de les extraire dans l'ordre voulu. 4 et 5 sont sur PE.

Q1

PD

PC

PP

PE

ECRIS

1

5

SOMME

4

- On dépile le sommet de PC et récupère SOMME.

Q1

PD

PC

PP

PE

ECRIS

1

5

4

- On exécute la méthode qui traite la commande SOMME avec les paramètres 4 et 5 qui sont dépilés.

Q1

PD

PC

PP

PE

ECRIS

1

- La méthode appelée laisse le résultat 9 sur la pile PD.

Q1

PD

PC

PP

PE

9

ECRIS

1

- Une donnée ayant été trouvée, on décrémente le sommet de la pile PP qui vaut à présent encore 0.

Q1

PD

PC

PP

PE

9

ECRIS

0

- Comme le sommet de la pile vaut 0, on dépile ce 0 inutile. On sait par ailleurs qu'on a assez de paramètres pour la primitive au sommet de PC : on transfère donc le nombre de paramètres attendus de PD vers PE, ce qui permet de les extraire dans l'ordre voulu. 9 est donc sur PE.

Q1

PD

PC

PP

PE

ECRIS

9

- On dépile le sommet de PC et récupère ECRIS.

- On exécute la méthode qui traite la commande ECRIS avec le paramètre 9.

- La méthode ECRIS affiche 9 sans laisser de résultat sur la pile.

- Toutes les piles sont vides.

L'interpréteur est en situation d'attente.

On s'aperçoit qu'après le découpage du texte d'entrée en unités lexicales, le travail essentiel de l'interpréteur est de répartir, ajouter, transférer et supprimer correctement ces unités de plusieurs piles. Son mécanisme peut par conséquent être ramené à une série simple (et très répétitive !) d'actions sur le texte fourni en entrée. Les actions les plus complexes, celles qui renvoient au traitement à effectuer par une primitive, seront déléguées à des méthodes qui manipuleront elles-mêmes les piles d'exécution et de paramètres.

VI-B - Au cœur de l'interpréteur

En ce qui concerne le découpage de la ligne d'entrée, les listes telles que définies dans l'unité GVLists sont à même de fournir les unités lexicales désirées.

Les piles sont disponibles dans l'unité GVStacks. La file d'attente sera prise en charge par une pile où les données seront stockées dans l'ordre inverse de la ligne fournie en entrée.

Le fichier inclus GVPrims.inc contiendra les méthodes en charge d'exécuter le code nécessaire au traitement des primitives.

L'unité GVAutomat contiendra les classes nécessaires à la mise en place de l'interpréteur. La plus importante est TGVAutomat qui constitue le cœur de l'interpréteur. Au cœur même de cette classe, on trouve la méthode Process qui boucle sur les éléments d'une ligne d'entrée.

Le schéma qui précède donne un aperçu du fonctionnement de l'interpréteur. L'interprétation est préparée par la méthode DoBegin, mise en œuvre par Process et terminée par DoEnd. Les 16 méthodes du processus sont décrites ci-après.

VI-B-1 - Les structures utiles

// *** description d'un espace d'interprétation ***
TGVAutomatRec = record
fNum: Integer; // élément en cours dans la ligne
fItem: string; // donnée en cours
fLine: string; // ligne en cours
fPrim: string; // primitive en cours
fProc: string; // procédure en cours
fLevel: Integer; // niveau en cours
end;

TGVAutomatRec est un enregistrement qui rassemble les éléments utiles à la description de l'état de l'interpréteur à un instant donné. fLevel correspond au niveau d'imbrication des appels à la méthode Process. Les méthodes associées à l'exécution des primitives font un usage intensif de fItem pour spécifier l'élément de la ligne d'entrée en cours de traitement.

// *** messages pour l'état de l'automate ***
CStatesArray: array[TGVAutomatState] of string =
(MasWaiting, MasEnding, MasWorking, MasError, MasWord, MasList,
MasVar,    MasNumber, MasEval, MasProc, MasPrim, MasPushing,
MasStopped, MasExePrim,    MasExeProc, MasPreparing, MasProcDone,
MasPrimDone, MasPrimStop, MasPrimValue,    MasFollowing,
MasEndFollowing);

Le tableau CStatesArray contient les chaînes de caractères qui caractérisent les différents états possibles de l'interpréteur. Il est synchronisé avec l'énumération TGVAutomatState :

// *** état de l'automate ***
TGVAutomatState = (asWaiting, asEnding, asWorking, asError,
asWord, asList, asVar, asNumber, asEval, asProc, asPrim,
asPushing, asStopped, asExePrim, asExeProc, asPreparing,
asProcDone, asPrimDone, asPrimStop, asPrimValue, asFollowing,
asEndFollowing);

Chacune des valeurs de l'énumération sera utilisée par une méthode du processus d'interprétation, permettant la mise en œuvre d'un suivi de l'exécution, lui-même indispensable à un mécanisme de débogage.

Comme dans l'exemple de traitement d'une ligne de commandes en GVLOGO, les piles utilisées sont au nombre de cinq. La queue suggérée dans l'introduction au fonctionnement de l'interpréteur est remplacée par une pile, les éléments étant empilés à rebours de leur agencement dans la ligne d'entrée.

fParamsStack: TGVIntegerStack; // pile des paramètres
fDatasStack: TGVStringStack; // pile des données
fCommandsStack: TGVStringStack; // pile des commandes
fExeStack: TGVStringStack; // pile d'exécution
fWkStack: TGVStringStack; // pile de travail

VI-B-2 - DoBegin

La tâche essentielle de la fonction DoBegin est de préparer l'interpréteur en empilant en ordre inverse sur fWkStack les éléments de la ligne d'entrée fournie en paramètre. Elle vérifie par ailleurs que la ligne traitée est une liste correcte, (ré)initialise des indicateurs utiles à certaines primitives (SINON, SI.VRAI, SI.FAUX) et ceux de fWkRec de type TGVAutomatRec.

function TGVAutomat.DoBegin(const St: string): Boolean;
// *** préparation de l'automate ***
var
LL : TGVList;
Li: Integer;
begin
fWkRec.fItem := St; // élément analysé
State := asPreparing; // état
Result := False; // suppose une erreur
if St = EmptyStr then // rien à traiter
Exit; // on sort
LL := TGVList.Create; // liste de travail créée
try
LL.Text := St; // affectation du texte à la liste
if LL.IsValid then  // liste OK ?
begin
fWkStack.Push(CBreak); // marque de fin
for Li := LL.Count downto 1 do // on empile à l'envers
fWkStack.Push(LL[Li - 1]);
fWkRec.fLine := St; // on conserve la ligne à analyser
fWkRec.fNum := 0; // élément dans la ligne
fElse := CDisabledState; // sinon désactivé
fTest := CDisabledState; // test aussi
Result := True; // tout est OK
end
else
// [### Erreur: liste incorrecte ###]
SetError(CE_BadList, St);
finally
LL.Free; // liste de travail libérée
end;
end;

VI-B-3 - Process

Bien que brève, la méthode Process est celle par laquelle l'essentiel du travail est réalisé :

procedure TGVAutomat.Process(const St: string);
// *** lancement de l'automate d'interprétation ***
begin
if DoBegin(St) then // préparation correcte ?
begin
State := asWorking; // état
// on boucle si pile non vide, stop non demandé et pas d'erreur
while not (fWkStack.IsEmpty or Stop or (not Error.Ok)) do
begin
fWkRec.fItem := fWkStack.Pop; // élément dépilé
Inc(fWkRec.fNum); // numéro conservé
case fWkRec.fItem[1] of // premier caractère déterminant
CBreak: Break; // <========== fin de travail
CQuote: DoWord; // <====== littéral
CBeginList: DoList; // <====== liste
CColon: DoVar; // <====== variable
CPlus, CMinus, '0'..'9' : DoNumber; // <====== nombre
'a'..'z','A'..'Z',CUnderline, CDot : DoCommand; // <====== commande
CBeginPar: DoEval; // <====== valeur à évaluer
end;
end;
DoEnd; // postparation
end;
State := asWaiting; // état
end;

Après l'initialisation grâce à DoBegin, elle balaie la ligne d'entrée en dépilant ses éléments et en répartissant leur traitement entre les méthodes adéquates jusqu'à ce qu'elle n'ait plus rien à traiter ou qu'une demande d'arrêt (via le champ booléen Stop) lui soit parvenue ou qu'un erreur (via le champ booléen Error.ok) soit intervenue. Son travail se clôt par l'appel de la méthode DoEnd.

Afin de faciliter la sortie de la boucle, on utilise le caractère # (CBreak).

VI-B-4 - DoEnd

La méthode DoEnd s'occupe du nettoyage après l'interprétation. Il peut en effet arriver qu'une commande reste en suspens sans avoir reçu le nombre de paramètres requis. L'interpréteur aura cessé de boucler et DoEnd signalera cette erreur.

procedure TGVAutomat.DoEnd;
// *** postparation de l'automate ***
begin
State := asEnding; // état
// commandes en attente sans drapeau de valeur rendue, sans erreur et
// sans arrêt ?
if (not fReturnFlag) and (fCommandsStack.Count <> 0)
and Error.Ok and (not Stop) then
begin
fWkRec.fItem := fCommandsStack.Pop; // on récupère la commande en suspens
if fWkRec.fItem[1] = CLink then // une primitive ?
begin
fWkRec.fItem := Copy(fWkRec.fItem, 2, CMaxLengthPrim); // $ supprimé
fWkRec.fPrim := fWkRec.fItem; // une primitive
end
else
fWkRec.fProc := fWkRec.fItem; // une procédure
// [### Erreur: pas assez de données ###]
SetError(CE_NotEnoughDatas, fWkRec.fItem);
end;
end;

VI-B-5 - DoWord

DoWord se contente d'enlever le guillemet qui précède le mot à empiler et de fournir ce dernier en paramètre à PushConst.

procedure TGVAutomat.DoWord;
// *** traitement d'un mot ***
var
LW: TGVWord;
begin
State := asWord; // état
LW := TGVWord.Create; // mot de travail créé
try
LW.Text := fWkRec.fItem; // normalisation du mot
LW.Text := LW.WithoutQuote; // sans le "
PushConst(LW.FmtText); // constante empilée
finally
LW.Free; // mot de travail libéré
end;
end;

VI-B-6 - DoList

Encore plus simple que DoWord, DoList indique qu'une liste a été rencontrée avant de passer la main à PushConst.

procedure TGVAutomat.DoList;
// *** traitement d'une liste ***
begin
State := asList; // état
PushConst(fWkRec.fItem); // constante empilée
end;

VI-B-7 - DoVar

Après avoir ôté les : devant l'unité en cours, DoVar cherche la variable locale puis, si nécessaire, la variable globale correspondante. Si trouvée, la valeur de la variable est empilée grâce à PushConst, sinon une erreur est déclenchée.

procedure TGVAutomat.DoVar;
// *** traitement d'une variable ***
var
LW: TGVWord;
begin
State := asVar; // état
LW := TGVWord.Create; // mot de travail créé
try
LW.Text := fWkRec.fItem; // nom analysé
LW.Text := LW.WithoutColon; // : retirés
if fLocVars.IsLocVar(LW.Text) then // variable locale ?
PushConst(fLocVars.DirectValLocVar) // on empile sa valeur
else
if fKernel.IsVar(LW.Text) then // variable globale ?
PushConst(fKernel.ValVar(LW.Text)) // on empile sa valeur
else
// [### Erreur: variable inconnue ###]
SetError(CE_UnknownVar, LW.Text);
finally
LW.Free; // libération du mot de travail
end;
end;

VI-B-8 - DoNumber

DoNumber vérifie que l'unité en cours est un nombre. Si c'est le cas, l'unité est envoyée à PushConst, sinon une erreur est déclenchée.

procedure TGVAutomat.DoNumber;
// *** traitement d'un nombre ***
var
LW: TGVWord;
begin
State := asNumber; // état
LW := TGVWord.Create; // mot de travail créé
try
LW.Text := fWkRec.fItem; // normalisation du mot
if LW.IsNumber then // un nombre ?
PushConst(LW.Text) // on empile la constante
else
// [### Erreur: nombre invalide ###]
SetError(CE_BadNumber, LW.Text);
finally
LW.Free; // mot de travail libéré
end;
end;

VI-B-9 - DoEval

DoEval est elle aussi une méthode très simple : elle envoie à PushConst le résultat de l'évaluation de l'expression, sauf en cas d'erreur. Par le mécanisme déjà étudié, cette dernière aura été signalée par l'évaluateur lui-même.

procedure TGVAutomat.DoEval;
// *** traitement d'une expression ***
begin
State := asEval; // état
fEval.Text := fWkRec.fItem; // on affecte à l'évaluateur
fEVal.Scan; // on évalue
if fEval.Error.Ok then  // pas d'erreur ?
PushConst(FloatToStr(fEval.Res)); // valeur empilée
end;

VI-B-10 - PushConst

On a vu que nombreuses étaient les méthodes qui envoyaient le résultat de leur travail à la méthode PushConst. Non seulement cette méthode empile la donnée fournie en entrée, mais elle procède à une série de tests indispensables au bon fonctionnement de l'interpréteur :

- au retour d'une procédure qui renvoie une donnée, il est normal que la pile des données contienne un élément : le champ privé fReturnFlag est l'indicateur dont il faut tenir compte en ce cas ;

- s'il n'y a plus de commandes en attente ou si aucun paramètre n'est attendu, il faut signaler cette erreur ;

- dans tous les autres cas, il faut décrémenter le sommet de la pile des paramètres et, s'il n'y a plus de paramètres en attente, exécuter la commande dont les paramètres ont tous été fournis.

procedure TGVAutomat.PushConst(const St: string);
// *** empilement d'une constante ***
begin
fWkRec.fItem := St;
State := asPushing; // état
fDatasStack.Push(St); // on empile la constante
if fReturnFlag then // si drapeau de valeur retournée
Exit; // on sort
// pas de paramètre ou pas de commande en attente ?
if (fParamsStack.Count = 0) or
((fParamsStack.Peek > 0) and (fCommandsStack.Count = 0)) then
begin
fWkRec.fPrim := EmptyStr; // pas de primitive en cours
// [### Erreur : que faire de ? ###]
SetError(CE_WhatAbout, St);
end
else
begin
fParamsStack.Push(fParamsStack.Pop - 1); // un paramètre a été trouvé
if (fParamsStack.Peek = 0) then // plus de paramètres en attente ?
// on exécute la commande en attente
ExeCommand;
end;
end;

VI-B-11 - DoCommand

La méthode DoCommand est un simple aiguillage qui vérifie si l'unité lexicale rencontrée est une primitive ou une procédure, déclenchant une erreur sinon.

procedure TGVAutomat.DoCommand;
// *** répartit les commandes ***
begin
if fKernel.IsPrim(fWkRec.fItem) then // une primitive ?
DoPrim // on la traite
else
if fKernel.IsProc(fWkRec.fItem) then // une procédure ?
DoProc // on la traite
else
// [### Erreur: ni une primitive ni une procédure ###]
//     SetError(CE_NorPrimNorProc, fWkRec.fItem);
end;

VI-B-12 - DoPrim

DoPrim s'occupe spécifiquement des primitives. Elle recherche le nombre de paramètres associés à la primitive et l'empile deux fois sur la pile des paramètres : le sommet de la pile sert de compteur lors du recouvrement des paramètres tandis que le second empilement évite de rechercher de nouveau à la fin du recouvrement le nombre de paramètres associés.

Afin de distinguer rapidement les primitives des procédures, le caractère $ est placé en tête du nom de la primitive et empilé.

Enfin, si aucun paramètre n'est attendu, la procédure est exécutée immédiatement.

procedure TGVAutomat.DoPrim;
// *** traitement d'une primitive ***
var
Li: Integer;
begin
State := asPrim; // état
// nombre de paramètres associés
Li := fKernel.NumParamsPrim(fWkRec.fItem);
fParamsStack.Push(Li); // on enregistre le nombre d'arguments attendus
fParamsStack.Dup; // on double ce nombre
// $ + nom de la primitive empilé
fCommandsStack.Push(CLink + fWkRec.fItem);
if Li = 0 then // pas de paramètres ?
ExePrim; // exécute immédiatement la primitive
end;

VI-B-13 - DoProc

DoProc est similaire à DoPrim en dehors du fait qu'elle empile le nom de la procédure sans préfixe.

procedure TGVAutomat.DoProc;
// *** traitement d'une procédure ***
var
Li: Integer;
begin
State := asProc; // état
// nombre de paramètres associés
Li := fKernel.ParamsCount(fWkRec.fItem);
fParamsStack.Push(Li); // on enregistre le nombre d'arguments attendus
fParamsStack.Dup; // on double ce nombre
// nom de la procédure empilée
fCommandsStack.Push(fWkRec.fItem);
if Li = 0 then // pas de paramètres ?
ExeProc; // exécute immédiatement la procédure
end;

VI-B-14 - ExeCommand

ExeCommand fonctionne à la manière de DoCommand en tant que simple aiguillage capable de distinguer les primitives des procédures. C'est le caractère $ introduit en préfixe par DoPrim qui permet cette distinction.

procedure TGVAutomat.ExeCommand;
// *** exécution d'une commande ***
begin
if fCommandsStack.Peek[1] = CLink then // $ ?
ExePrim // c'est une primitive
else
ExeProc; // sinon une procédure
end;

VI-B-15 - ExePrim

La méthode ExePrim est chargée d'exécuter une primitive. Après avoir retiré le préfixe du nom de la primitive, elle récupère sur la pile fParamsStack le nombre de paramètres associés à la primitive. Par ailleurs, elle crée les outils nécessaires à de nombreuses primitives pour les libérer à la fin de l'exécution. Au centre de la méthode se situe l'appel à la méthode DoExePrim qui procède à l'exécution à proprement parler.

procedure TGVAutomat.ExePrim;
// *** exécution d'une primitive ***
var
Li, LPrm: Integer;
LW: TGVWord;
LL: TGVList;
LU: TGVListUtils;

{$I Prims.inc}  // liste des méthodes pour les primitives

begin
// nom de la primitive (sans le $)
fWkRec.fPrim := Copy(fCommandsStack.Pop, 2, CMaxLengthPrim);
State := asExePrim; // état
try
Li := fKernel.NumPrim(fWkRec.fPrim); // numéro retrouvé
fParamsStack.Pop; // on libère les paramètres
LPrm := fParamsStack.Pop; // nombre de paramètres de la primitive récupéré
while LPrm <> 0 do // tant qu'il y a des paramètres
begin
// on empile les paramètres sur la pile d'exécution
fExeStack.Push(fDatasStack.Pop);
Dec(LPrm); // paramètre suivant
end;
LL := TGVList.Create; // liste de travail
try
LW := TGVWord.Create; // mot de travail
try
LU := TGVListUtils.Create; // utilitaire de travail
try
DoExePrim(Li); // exécution
finally
LU.Free; // libération de l'utilitaire de travail
end;
finally
LW.Free; // libération du mot de travail
end;
finally
LL.Free; // libération de la liste de travail
end;
Application.ProcessMessages; // permet l'affichage fluide
finally
State := asPrimDone; // état
end;
end;

VI-B-16 - DoExePrim

DoExePrim est elle aussi un aiguillage. Elle opère entre toutes les primitives disponibles, distinguées par le numéro qui leur est propre. Tout le travail en amont a laissé les piles dans l'état attendu par la primitive à exécuter : elle n'a qu'à effectuer sa tâche tout en vérifiant que les données fournies en nombre suffisant sont conformes aux types attendus.

procedure DoExePrim(const N: Integer);
// *** exécution d'une primitive ***
begin
// on exécute la primitive
case N of
1: DoPrimGVLogo; // GVLOGO
2: DoPrimTopLevel; // NIVEAU.SUP
3: DoPrimStop; // STOP
4: DoPrimIf; // SI
5: DoPrimReturn; // RENDS
6: DoPrimWord; // MOT
7: DoPrimRepeat; // REPETE
9: DoPrimFor; // BOUCLE
10: DoPrimAllProcsEdit; // EDITE.TOUT
11: DoPrimLoadProcs; // CHARGE.PROCS
12: DoPrimSaveProcs; // SAUVE.PROCS
13, 243: DoPrimProcToEdit; // EDITE.PROC
14: DoPrimParamLineProc;  // PARAMS.PROC
15: DoPrimDefListProc; // DEF.PROC

VI-B-17 - ExeProc

Pendant de ExePrim, ExeProc est bien plus complexe. Après quelques actualisations de données utiles pour le débogage du programme GVLOGO en cours, comme sa consœur, elle récupère le nombre de paramètres nécessaires, mais doit créer les variables locales correspondantes.

procedure TGVAutomat.ExeProc;
// *** exécution d'une procédure ***
var
LPm, Li, Lj: Integer;
LS: string;
LL: TGVList;
LOldProc: string;
begin
LOldProc := fWkRec.fProc; // on conserve l'ancienne procédure
Inc(fWkRec.fLevel); // niveau suivant
fWkRec.fProc := fCommandsStack.Pop; // nom de la procédure en cours
fWkRec.fItem := fWkRec.fProc; // élément en cours actualisé
State := asExeProc; // état
try
fParamsStack.Pop; // on nettoie le sommet de la pile des paramètres
LPm := fParamsStack.Pop; // nombre de paramètres récupéré
fLocVars.AddLocNumber(LPm); // sauvegarde dans pile des variables locales
// récupération des paramètres et création des variables locales
if LPm <> 0 then // s'il y a des paramètres
for Li := LPm downto 1 do // ordre inversé !
with fKernel do
// variables locales initialisées
fLocVars.AddLocVar(ParamNum(fWkRec.fProc, Li), fDatasStack.Pop);

Avant de l'exécuter, ExeProc doit encore balayer les lignes de la procédure en évitant les commentaires et en tenant compte d'une éventuelle demande d'arrêt. Le plus important est qu'elle appelle de nouveau Process : on a donc un mécanisme récursif. Rien ne dit que ce nouvel appel à la méthode de départ n'enclenchera pas une cascade d'appels du même genre, d'où la nécessité d'employer des piles et de traiter correctement les variables locales qui pourront être homonymes sans renvoyer à la même réalité, donc à la même valeur.

// récupération des lignes de la procédure
LL := TGVList.Create;
try
LS := EmptyStr; // définition vide
// balayage des lignes de la procédure
for Li := 1 to fKernel.ProcLinesCount(fWkRec.fProc) do
begin
if not Stop then
begin
// texte de la ligne en cours
LL.Text := fKernel.ProcLine(fWkRec.fProc, Li);
for Lj := 1 to LL.Count do // on balaie la ligne
if Trim(LL[Lj - 1]) <> CComment then // commentaire ?
LS := LS + CBlank + LL[Lj - 1]
else
Break; // on sort si commentaire
end;
end;
// exécution de la procédure
Process(CBeginList + LS + CEndList);
finally
LL.Free; // libération de la liste
end;

À la fin de l'exécution, il faut détruire les éventuelles variables locales, conserver la possible valeur de retour en l'empilant et récupérer les données pour le débogage :

if fLocVars.LocVarsCount > 0 then // s'il y a des variables en suspens
fLocVars.DelLastGroup; // variables locales supprimées
if fReturnFlag then // valeur en attente ?
begin
fReturnFlag := False; // drapeau baissé
PushConst(fReturnVal); // valeur empilée
end;
finally
State := asProcDone; // état
Dec(fWkRec.fLevel); // niveau précédent
fWkRec.fProc := LOldProc; // récupération de l'ancienne procédure
end;
end;

VI-C - Les primitives

VI-C-1 - Utilitaires

On a vu qu'une primitive est exécutée grâce à la méthode DoExePrim et que le travail préparatoire a permis l'obtention des données nécessaires sur fExeStack. Sans entrer dans le détail du fonctionnement de toutes les primitives, on peut s'intéresser à des constantes.

Trois méthodes sont très utilisées :

function IsList: Boolean;
// *** est-ce une liste ? ***
begin
Result := (fWkRec.fItem <> EmptyStr) and (fWkRec.fItem[1] = CBeginList);
end;

procedure AData;
// *** dépile et normalise un mot ***
begin
fWkRec.fItem := fExeStack.Pop; // donnée dépilée
LW.Text := fWkRec.fItem; // mot normalisé
end;

function IsListPop: Boolean;
// *** dépilement et test si liste ***
begin
AData; // donnée dépilée
Result := IsList; // test
end;

Les données dépilées sont stockées dans fWkRec.fItem, permettant ainsi une récupération facile pour le débogage. Les primitives travailleront essentiellement avec ce champ d'enregistrement.

VI-C-2 - La classe TGVAutomatMessage

Un autre aspect à relever est l'emploi d'une classe particulière pour communiquer avec l'unité chargée d'afficher les textes. L'interpréteur ne peut en effet savoir comment et par quel outil le texte à afficher sera traité. On peut même souhaiter garder une grande souplesse dans ce domaine afin d'élargir le champ des possibilités, comme rediriger les sorties vers une imprimante.

La solution adoptée fait appel à une classe dont l'objectif est d'envoyer des ordres génériques associés à un texte. En retour, elle accepte aussi la récupération de messages.

Une solution à étudier serait de se servir des systèmes de messages déjà opérationnels nativement.

La classe TGVAutomatMessage traite par conséquent de la communication de la plupart des données concernant les entrées/sorties de texte. En voici l'interface :

type
// *** message de l'automate ***
TGVAutomatMessage = class(TObject)
strict private
fCmd: TGVAutomatCmd; // commande en cours
fMessage: string; // message associé
fOnChange: TNotifyEvent;
procedure SetCmd(AValue: TGVAutomatCmd);
procedure SetMessage(AValue: string);
protected
procedure Change; // changement notifié
public
constructor Create; // création
destructor Destroy; override; // destructeur
procedure Clear; // nettoyage
// données regroupées
procedure SetMessageAll(const ACmd: TGVAutomatCmd; AMess: string);
// message
property Message: string read fMessage write SetMessage;
//commande
property Cmd: TGVAutomatCmd read fCmd write SetCmd default acNone;
// gestionnaire de changement
property OnMessageChange: TNotifyEvent read fOnChange write fOnChange;
end;

Associé à la chaîne de caractères Message, le champ fondamental Cmd est du type TGVAutomatCmd, une énumération dont voici l'interface :

// *** commandes de l'interpréteur ***
TGVAutomatCmd = (acNone, acClear, acWrite, acType, acReadList, acReadChar,
acConfirm, acBold, acUnderline, acItalic, acNoBold, acNoUnderline,
acNoItalic, acColor, acSetColor, acBackColor, acSetBackColor,
acSetFontSize, acFontSize, acSetFont, acFont, acWriteEdit);

Du côté du traitement des primitives, on aura un code du type :

procedure DoPrimWrite;
// *** ECRIS ***
begin
if IsListPop then // liste ?
fWkRec.fItem := LU.ListToStr(fWkRec.fItem); // on enlève les crochets
LW.Text := fWkRec.fItem; // mot normalisé
fWkRec.fItem := LW.Text; // sans les caractères d'échappement
if fTurtleOutput then  // écran de la tortue ?
fTurtle.Text(fWkRec.fItem)
else
Message.SetMessageAll(acWrite, fWkRec.fItem);
end;

L'écriture du message passe par la méthode SetMessageAll qui attend deux paramètres : le type de la commande et le texte associé.

L'unité en charge de traiter les textes reçoit des messages qu'elle interprète comme elle l'entend. Elle peut en retour affecter une valeur à la propriété Message de la classe TGVAutomat.

Voici par exemple comment le programme de test de GVAutomat opère :

procedure TMainForm.GetMessage(Sender: TObject);
// gestionnaire des messages
begin
case Automat.Message.Cmd of
// écriture
acWrite: mmoMain.Lines.Add(Automat.Message.Message);
// nettoyage
acClear: mmoMain.Lines.Clear;
// lecture d'une liste
acReadList: Automat.Message.Message := InputBox('TestGVAutomat',
'Entrez la valeur demandée ici :', EmptyStr);
// demande de confirmation
acConfirm: if MessageDlg(Automat.Message.Message , mtConfirmation,
mbYesNo, 0) = mrYes then
Automat.Message.Message := CStTrue // vrai en retour
else
Automat.Message.Message := CStFalse; // faux en retour

On voit dans cette portion de code qu'il s'agit essentiellement de répartir le traitement des commandes reçues. Ici, la commande acWrite conduit à l'écriture dans un composant de type TMemo alors que la commande acReadList renvoie le résultat obtenu lors de la saisie dans une boîte de type TInputBox.

VI-C-3 - Deux primitives ordinaires… et une troisième un peu moins

Dans l'ensemble, le code des méthodes qui traitent les primitives sont simples. À titre d'exemple, voici celui de la primitive AVANCE :

procedure DoPrimForward;
// *** AVANCE AV ***
begin
AData; // un mot attendu
if LW.IsNumber then
fTurtle.Move(LW.AsNumber)
else
// [### Erreur: nombre incorrect ###]
SetError(CE_BadNumber, LW.Text);
end;

On extrait la donnée attendue, vérifie qu'il s'agit d'un nombre et l'envoie si possible à l'unité gérant la tortue.

De la même façon, la primitive HASARD est traitée ainsi :

procedure DoPrimRandom;
// *** HASARD ***
begin
if IsListPop then  // une liste ?
begin
LL.Text := fWkRec.fItem; // on l'analyse
PushConst(LL.AtRandom); // on renvoie un élément au hasard
end
else
begin
LW.Text := fWkRec.fItem; // mot normalisé
if LW.IsInt then
PushConst(IntToStr(Random(LW.AsInt) + 1)) // nombre au hasard
else
PushConst(LW.Atrandom); // on renvoie un caractère au hasard
end;
end;

Comme cette primitive est susceptible de recevoir une liste ou un mot comme paramètre et que le mot peut être un nombre, deux tests doivent être opérés en son sein. À la fin, une valeur aura été empilée grâce à PushConst, prête pour une autre primitive.

Enfin, la primitive EXEC (EXECUTE) mérite un peu d'attention, car elle opère de manière originale :

procedure DoPrimExec;

// *** EXEC EXECUTE ***
var
Li: Integer;
begin
if IsListPop then // une liste ?
begin
if fWkRec.fItem <> CEmptyList then // liste vide ?
begin
LL.Text := fWkRec.fItem; // on l'analyse
if LL.IsValid then // liste valide ?
begin
for Li := LL.Count downto 1 do // on balaie cette liste à rebours
// on empile l'élément dans la liste de travail
fWkStack.Push(LL[Li - 1]);
end
else
// [### Erreur: liste incorrecte ###]
SetError(CE_BadList, fWkRec.fItem);
end
else
Exit; // on sort
end
else
// [### Erreur: pas une liste ###]
SetError(CE_UnknownList, fWkRec.fItem);
end;

Après avoir extrait la liste qui doit être exécutée, elle en insère les éléments dans la pile de travail. Ainsi, cette liste n'est plus comprise comme un ensemble de données, mais comme une suite de commandes exécutables par GVLOGO. On voit encore ici l'intérêt d'une structure de type pile (ou queue).

VI-C-4 - Le programme de test de l'unité GVAutomat

Le programme de test ne présente pas de difficultés particulières. On observera l'utilisation intensive des notifications :

procedure TMainForm.FormCreate(Sender: TObject);
// création de la fiche
begin
Automat := TGVAutomat.Create; // automate créé
Automat.OnStateChange := @GetStateChange; // état changé
Automat.Error.OnError := @GetError; // gestionnaire d'erreurs
Automat.Message.OnMessageChange := @GetMessage; // gestionnaire de messages
Automat.Follow := False; // pas de trace par défaut
fDeepTrace := False;
// on crée la tortue
GVTurtle := TGVTurtle.Create(imgTurtle.Width, imgTurtle.Height);
Automat.Turtle := GVTurtle; // tortue liée à l'automate
GVTurtle.OnChange := @TurtleState;  // gestionnaire de changement
GVTurtle.OnBeforeChange := @TurtleBeforePaint; // idem avant de dessiner
GVTurtle.Error.OnError := @GetError; // gestionnaire d'erreurs
GVTurtle.ReInit; // initialisation
GVTurtle.Kind := tkPng; // tortue image
end;

Les différents gestionnaires permettent, comme d'habitude, de centraliser les changements d'états, les erreurs et les messages divers générés par les modules qui composent l'application.

Pour faire fonctionner ce programme de test, il faut tout d'abord laisser l'unité analyser les procédures présentes dans l'éditeur en cliquant sur le bouton « Vers procédures ». Un clic sur l'autre bouton « Go ! » permet d'exécuter la ligne de commandes écrite dans le composant TEdit en haut à droite de l'écran.

L'utilisation des boutons « Trace » et « Trace profonde » permettent d'avoir un aperçu du suivi de l'exécution. Quant au bouton « Stop », il permet d'interrompre l'exécution du programme en cours.

Si le programme de test est l'embryon d'un logiciel de programmation digne de ce nom, il permet cependant à l'utilisateur d'écrire ses propres procédures.

La seule différence de comportement rencontrée entre Windows 8.1 et Linux Mint 17.1 concerne l'emploi de la virgule (Windows) ou du point (Linux) pour écrire les nombres décimaux.