IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Réaliser un interpréteur en Pascal

Avec Free Pascal sous Lazarus

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…

2 commentaires Donner une note à l´article (0)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

Image non disponible

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

 
 
Sélectionnez
  // *** 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.

 
Sélectionnez
  // *** 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 :

 
Sélectionnez
  // *** é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.

 
Sélectionnez
      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.

 
Sélectionnez
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é :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.
 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
    // 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 :

 
Sélectionnez
    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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
  // *** 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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;

 
Sélectionnez
// *** 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 :

 
Sélectionnez
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.

Image non disponible

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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 ni 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.