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.