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

- V-A. Les variables locales

- V-A-1. Définition

- V-A-2. Les variables locales dans GVLOGO

- V-A-3. Exemples

- V-A-4. Implémentation des variables locales

- V-A-4-a. La classe TGVLocVars

- V-A-4-b. Test de l'unité GVLocVars

- V-B. Le noyau

- V-B-1. Définition

- V-B-1-a. La structure de GVKernel

- V-B-1-b. Les méthodes de gestion de l'espace de travail

- V-B-1-c. Le programme de test

- V-B-2. Les variables globales

- V-B-2-a. Définition

- V-B-2-b. Les variables globales dans GVLOGO

- V-B-2-c. L'implémentation des variables globales

- V-B-3. Les procédures

- V-B-3-a. Définition

- V-B-3-b. Les procédures dans GVLOGO

- V-B-3-c. L'implémentation des procédures

- V-B-4. Les paquets

- V-B-4-a. Définition

- V-B-4-b. Les paquets dans GVLOGO

- V-B-4-c. L'implémentation des paquets

- V-B-5. Les primitives

- V-B-5-a. Définition

- V-B-5-b. Les primitives dans GVLOGO

- V-B-5-c. L'implémentation des primitives

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

Cette partie traite des composants du langage GVLOGO, depuis les variables locales jusqu'aux primitives, en passant par les variables globales, les procédures et les paquets.

V-A - Les variables locales

V-A-1 - Définition

Les variables locales associent un nom à une valeur dont la visibilité est limitée. Pour GVLOGO, une variable locale est une variable dont la portée est circonscrite à la procédure en cours d'exécution.

L'intérêt de ce concept est d'éviter les confusions qui naissent lorsque l'accès à une variable est global : utilisation accidentelle d'une donnée indésirable ou modification intempestive d'une valeur utile à un autre module.

LOGO étant un langage non typé, toute valeur assignée à la variable sera acceptée : nombre, mot, liste, booléen.

V-A-2 - Les variables locales dans GVLOGO

Les variables locales sont de deux ordres dans GVLOGO :

- il peut s'agir des paramètres attendus par une procédure : ces paramètres recueillent les valeurs fournies par le programme et sont détruits à la sortie de la procédure, garantissant ainsi, entre autres, que le programme n'est pas assujetti au choix de noms de paramètres prédéterminés pour nommer les siens ;

- ce peut être aussi une variable définie explicitement comme locale grâce à la primitive LOCALE : cette dernière doit apparaître dans le corps de la procédure et être suivie du nom de la variable (qui ne peut être un homonyme d'un des paramètres de la procédure).

V-A-3 - Exemples

Voici tout d'abord un exemple de variables locales définies comme paramètres d'une procédure :

Ici, les variables n et cote sont locales. Elles n'ont par conséquent de réalité qu'à l'intérieur des procédures pour desquelles elles sont définies. En particulier, les deux variables n des procédures ci-dessus sont totalement indépendantes l'une de l'autre. Une même variable ne peut apparaître deux fois sur la ligne des paramètres d'une procédure.

Voici à présent la définition explicite d'une variable locale :

La variable choix n'aura d'existence que dans cette procédure. Sa valeur sera réinitialisée à chaque appel : elle sera équivalente à la chaîne vide.

En LOGO, les variables prennent deux formes : soit leur nom est précédé d'un " et il s'agit alors de travailler avec la variable, soit il est précédé de : et il s'agit de travailler avec le contenu auquel elle renvoie. Autrement dit, soit on se réfère à l'adresse mémoire de la variable, soit à son contenu. Cette distinction est aussi valable pour les variables globales.

V-A-4 - Implémentation des variables locales

Le choix pouvait être fait d'incorporer les variables locales au noyau de GVLOGO. Cependant, contrairement aux variables globales dont la gestion est simple, les variables locales posent un problème particulier : du fait de la récursivité des appels de procédures et de leur éventuelle imbrication, des variables de même nom et en nombre non précisé peuvent coexister au même instant. En fait, rien n'empêche une procédure d'en appeler une autre dont les paramètres auront des noms homonymes de la procédure d'accueil. Il faut donc créer un système d'empilement capable de mémoriser toutes les valeurs pendantes et de les restituer au bon moment. On peut à cette fin se servir des piles utilisées par l'interpréteur, mais, dans le cadre de notre étude et pour une meilleure lisibilité du code, il a semblé préférable de proposer une unité réservée à part entière aux variables locales.

V-A-4-a - La classe TGVLocVars

La classe chargée de gérer les variables locales s'appelle TGVLocVars. Le principe de son fonctionnement est le suivant :

- réservation de la place pour X variables locales ;

- création des X variables sur deux piles (une pour les noms, une pour les valeurs, toutes deux synchronisées) ;

- enregistrement/lecture des valeurs des variables ;

- effacement du groupe des X variables à la sortie.

Le travail se fait donc par groupes. Voici l'interface de cette classe :

type
// *** classe pour les variables locales ***
TGVLocVars = class(TObject)
strict private
fCount: Integer; // compteur de variables attendues
fWhere: Integer; // résultat dernière recherche
fError: TGVErrors; // enregistrement d'une erreur
fNames: TGVStringStack; // liste des noms de variables locales
fValues: TGVStringStack; // liste des valeurs des variables locales
fStack: TGVIntegerStack; // pile des groupes de variables locales
fOnChange: TNotifyEvent; // gestionnaire de changement
procedure SetCount(AValue: Integer); // compte des variables attendues
// localisation d'une variable locale
function Where(const Name: string): Integer;
protected
procedure Change; // changement notifié
public
// création
constructor Create;
// destruction
destructor Destroy; override;
// nettoyage
procedure Clear;
// réservation pour N variables locales
procedure AddLocNumber(N: Integer);
// réservation pour une nouvelle variable locale
procedure AddNewLocNumber;
// est-ce une variable locale ?
function IsLocVar(const Name: string): Boolean;
// nombre de variables locales
function LocVarsCount: Integer;
// affectation à une variable locale
function AddLocVar(const Name, Value: string): Boolean;
// mise à jour d'une variable locale
function UpdateLocVar(const Name, Value: string): Boolean;
// mise à jour directe d'une variable locale (existence préétablie)
procedure DirectUpdateLocVar(const Value: string);
// valeur d'une variable locale
function ValLocVar(const Name: string; out Value: string): Boolean;
overload;
function ValLocVar(const Name: string): string; overload;
// valeur directe d'une variable (existence préétablie)
function DirectValLocVar: string;
// destruction du dernier groupe de variables locales
procedure DelLastGroup;
// liste des variables locales
function LocVarsToList: string;
// événement si changement
property OnChange: TNotifyEvent read fOnChange write fOnChange;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
// compteur de variables attendues
property Count: Integer read fCount write SetCount default 0;
end;

On utilise d'abord la méthode chargée d'allouer la place des variables. S'il s'agit des paramètres d'une procédure, la méthode est AddLocNumber :

procedure TGVLocVars.AddLocNumber(N: Integer);
// *** réservation pour N variables locales ***
begin
if Count = 0 then // compteur à zéro ?
begin
fStack.Push(N); // on empile le nombre de nouvelles variables
Count := N; // compteur à jour
end
else
// [### Erreur: mauvais compteur ###]
Error.SetError(CIE_MemLocVar, ToString);
end;

On empile le nombre de paramètres voulus et on fixe le compteur Count à ce nombre.

S'il s'agit d'une variable locale explicite, la place à réserver exige qu'on soit à l'intérieur d'une procédure : on incrémente alors le sommet de la pile des variables attendues. Afin de faciliter l'identification de cet état, même une procédure qui n'aura aucun paramètre en réservera 0 !

C'est ce travail que fait la méthode AddNewLocNumber :

procedure TGVLocVars.AddNewLocNumber;
// *** nouvelle place pour une variable locale ***
begin
if fStack.IsEmpty then // pas de variables locales en cours ?
// [### Erreur: allocation interdite ###]
Error.SetError(CE_LocVarForbidden, P_Loc)
else
begin
Count := 1; // place allouée
fStack.Push(fStack.Pop + 1); // sommet de pile incrémenté
end;
end;

Les autres méthodes concernent les variables proprement dites. On peut les ajouter, les modifier, les consulter et les supprimer. Un système de notifications permet de signaler tout changement, ce qui est indispensable pour l'exploitation future de l'unité par l'interpréteur.

Une fois la place réservée, on peut ajouter la ou les variables :

function TGVLocVars.AddLocVar(const Name, Value: string): Boolean;
// *** ajout d'une variable locale ***
var
LW: TGVWord;
begin
Result := False; // erreur présumée
if Count = 0 then // plus de variables attendues ?
begin
// [### Erreur: mauvais compteur ###]
Error.SetError(CIE_MemLocVar, ToString);
Exit;
end
else
Count := Count - 1; // on décompte la variable
LW := TGVWord.Create; // mot de travail créé
try
LW.Text := Name; // mot normalisé
LW.Text := LW.WithoutColon; // on supprime les deux points
LW.Text := LW.WithoutQuote; // et les éventuels guillemets
if LW.IsValidIdent then // le nom est-il correct ?
begin
// on crée la variable locale
fNames.Push(LW.Text); // on empile le nom
fValues.Push(Value); // puis la valeur
Result := True;
Change; // notifie le changement si effectif
end
else
// [### Erreur: nom incorrect ###]
Error.SetError(CE_BadName, Name);
finally
LW.Free; // libération du mot de travail
end;
end;

La modification de la valeur associée à une variable locale s'opère en recherchant son nom dans la liste des variables grâce à la méthode Where. En effet, il ne faut pas chercher au-delà de la portée fixée par la réservation initiale :

function TGVLocVars.Where(const Name: string): Integer;
// *** localisation d'une variable locale ***
var
Li: Integer;
LW: TGVWord;
begin
Result := -1; // non trouvée par défaut
LW := TGVWord.Create; // création du mot de travail
try
LW.Text := Name; // mot normalisé
LW.Text := LW.WithoutColon; // sans les deux points
LW.Text := LW.WithoutQuote; // sans les guillemets
// on balaie les variables locales s'il y en a
if  (not fStack.IsEmpty) and (fStack.Peek <> 0) then
begin
for Li :=  1 to fStack.Peek do
if AnsiSameText(fNames[fNames.Count - Li], LW.Text) then // trouvé ?
begin
fWhere := fNames.Count - Li; // emplacement trouvé et mémorisé
Result := fWhere; // résultat
Break; // on a fini
end;
end;
finally
LW.Free; // libération du mot de travail
end;
end;

La modification s'appuie alors sur cette recherche grâce à la méthode DirectUpdateLocVar :

procedure TGVLocVars.DirectUpdateLocVar(const Value: string);
// *** mise à jour directe d'une variable locale ***
// => suppose un test préalable de son existence
begin
fValues[fWhere] := Value; // nouvelle valeur
Change; // changement notifié
end;

La recherche d'une valeur suit le même schéma en utilisant la méthode DirectValLocVar :

function TGVLocVars.DirectValLocVar: string;
// *** valeur directe d'une variable ***
// => suppose un test préalable de son existence
begin
Result := fValues[fWhere];
end;

La libération du dernier groupe de variables se fait avec la méthode DelLastGroup :

procedure TGVLocVars.DelLastGroup;
// *** suppression des dernières variables locales ***
var
Li: Integer;
begin
if fStack.IsEmpty or (fNames.Count < fStack.Peek) then
// [### Erreur: pas assez de variables internes ###]
Error.SetError(CIE_LowStack, ToString)
else
if fStack.Peek = 0 then // pas de variables locales ?
fStack.Pop // on dépile l'indicateur
else
for Li := 1 to fStack.Pop do  // on supprime les variables
begin
fNames.Pop; // nom dépilé
fValues.Pop; // valeur dépilée
end;
Change; // changement notifié
end;

V-A-4-b - Test de l'unité GVLocVars

Le programme de test est une nouvelle fois décliné en deux versions :

- Lazarus pour Windows 32 ;

- Lazarus pour Linux.

Pour l'utiliser correctement, il ne faut pas oublier de réserver la place des variables que l'on souhaite ajouter : sinon, il faut relire ce qui précède…

V-B - Le noyau

V-B-1 - Définition

Le noyau est contenu dans l'unité GVKernel, qui est la plus volumineuse du projet.

Le noyau est le centre névralgique du programme GVLOGO. Toute définition nouvelle, toute action sur les objets de GVLOGO transitent par lui. Il concentre en son sein l'ensemble des définitions des objets (variables, procédures, listes de propriétés et paquets) ainsi que les méthodes pour les manipuler. Il fournit par ailleurs les outils de travail applicables aux primitives du langage.

Le noyau est fondé sur une exploitation systématique des listes de propriétés. Si la vitesse d'exécution peut être plus faible qu'avec un traitement particulier pour chaque type d'objet, le noyau présente l'avantage d'offrir une souplesse remarquable : il devient par exemple possible de définir depuis un programme GVLOGO des extensions du logiciel lui-même en créant de manière dynamique des procédures ou des variables !

V-B-1-a - La structure de GVKernel

L'objet central de la classe TGVLogoKernel est un champ privé nommé fWorkZone de type TGVPropList. Les listes de propriétés servent à décrire et à stocker les objets utilisés par un programme GVLOGO. Cette zone de travail est accompagnée des outils habituels de gestion des erreurs et des notifications :

// *** TGVKernel ***
TGVLogoKernel = class(TObject)
strict private
fError: TGVErrors; // enregistrement d'une erreur
fErrPos: Integer; // position de l'erreur
fProtected: Boolean; // drapeau de protection
fWorkZone: TGVPropList; // zone de travail général
fOnChange: TNotifyEvent; // notification des changements
fTempList: TGVListUtils; // liste de travail

Une série de méthodes protégées permettent de gérer cet espace :

protected
// gestion des changements
procedure Change;
// est-ce un type d'objet donné ?
function IsObj(const Name, Kind: string): Boolean;
// compte d'objets d'un type donné
function CountObj(const Kind: string): Integer;
// ajoute un objet de type donné
function AddObj(const Name, Kind, Value: string): TGVError;
// supprime un objet de type donné
function RemoveObj(const Name, Kind: string): TGVError;
// supprime une liste d'objets de type donné
function RemoveSomeObjs(const L, Kind: string): TGVError;
// sauvegarde générique
function Save(const FileName, Lst, Kind: string): TGVError;
// chargement générique
function Load(const FileName, Kind: string): TGVError;
// supprime tous les objets d'un certain type
procedure RemoveAllObjs(const Kind: string);
// liste d'objets d'un type donné
function ObjsToList(const Kind: string): string;

Ces méthodes sont complétées par d'autres méthodes publiques qui permettent à l'utilisateur de manipuler l'espace dans son ensemble :

public
// *** gestion ***
// création
constructor Create;
// destruction
destructor Destroy; override;
// remise à zéro de l'espace de travail
procedure Clear;
// renvoie le nombre d'objets
function Count: Integer;
// renvoie tout l'espace de travail
procedure Dump(Lst: TStrings);
// l'objet existe-t-il ?
function Exists(const Name: string): Boolean;
// l'objet est-il protégé ?
function IsProtected(const Name: string): Boolean;
// renvoie les objets de l'espace de travail
function ToList: string;
// charge tout l'espace de travail
function LoadAll(const FileName: string): Boolean;
// sauvegarde tout l'espace de travail
function SaveAll(const FileName: string): Boolean;

Viennent ensuite des groupes de méthodes qui s'appuient sur les méthodes générales pour les adapter aux particularités des variables, des procédures, des primitives, des paquets et des listes de propriétés.

V-B-1-b - Les méthodes de gestion de l'espace de travail

Les méthodes générales de gestion de l'espace de travail manipulent donc des listes de propriétés. Les objets manipulés sont en effet différenciés par une propriété choisie parmi celles proposées dans GVConsts :

// *** noyau ***
CVr = CDot + 'VAR'; // variable
CBurried = CDot + 'BUR'; // enterré
CInPackage = CDot + 'INP'; // dans un paquet
CPackage = CDot + 'PKG'; // un paquet
CProc = CDot + 'PRC'; // une procédure
CExtLP = CDot + 'GVE'; // extension d'un espace de travail

La constante CDot étant un point, on voit que chaque type d'objet est caractérisé par une propriété dont le nom est ce point suivi de trois lettres. La valeur de la propriété est donnée par la liste qui suit ce nom.

L'exemple très simple du stockage d'une variable illustrera ce principe :

pas|[.VAR [61]]

pas est le nom de l'objet. Le signe | est le séparateur déjà rencontré lors de l'étude des listes de propriétés : il sépare le nom de la liste de sa définition. La liste [.VAR [61]] est bien constituée d'un nombre pair d'éléments : l'élément .VAR nomme la propriété d'une variable dont la valeur est fournie par la sous-liste [61], en l'occurrence 61.

Tout objet suit cette structure de base.

Il est alors possible d'écrire des méthodes génériques. Voici par exemple la méthode qui compte n'importe quel type d'objet :

function TGVLogoKernel.CountObj(const Kind: string): Integer;
// *** compte des objets d'un type donné ***
var
LS: string;
begin
Result := 0; // pas d'objets
for LS in fWorkZone do // on balaie la zone de travail
if (LS <> EmptyStr) and // nom de la liste non vide
// si c'est un objet du même type (CExtLP ne filtre rien)
((Kind = CExtLP) or fWorkZone.IsProp(LS, Kind)) then
Inc(Result); // on incrémente le résultat
end;

Même si certaines méthodes comme Load ou Save sont bien plus longues, leur fonctionnement reste fondé sur le même principe.

Ajouter un objet à l'espace de travail fWorkZone est un peu plus difficile puisque l'identification doit être unique et qu'il existe un système de protection des objets. Il faut donc créer l'objet s'il n'existe pas ou le mettre à jour seulement s'il n'est pas protégé et qu'il ne désigne pas un objet d'un autre type : il serait regrettable de détruire une procédure nécessaire au fonctionnement d'un programme par la création d'une vulgaire variable… D'où l'extrait suivant de la méthode AddObj :

if IsPrim(Name) then // est-ce une primitive ?
begin
// [### Erreur: une primitive ne peut pas être modifiée ###]
Result := CE_CantModifyPrim;
Exit; // on sort
end;
LWord := TGVWord.Create; // mot de travail créé
try
Result := CE_None; // on suppose qu'il n'y a pas d'erreur
if Exists(Name) then // le nom est-il pris ?
begin
if IsObj(Name, Kind) then // est-ce un objet de même nature ?
begin
if IsProtected(Name) then // protégé ?
// [### Erreur : l'objet est protégé ###]
Result := CE_Protected
else
begin
// sinon mettre à jour
if fWorkZone.UpDateListP(Name, Kind, Value) then
Change // notifie le changement
else
// [### Erreur: mauvaise liste ###]
Result := CE_UnknownListWord;
end;
end
else
// [### Erreur : l'objet est d'une autre nature ###]
Result := CE_BadObj;
end
else
begin // le nom n'est pas pris

Les primitives ne sont pas incluses dans le noyau. Elles ne sont pas représentées sous forme de liste de propriétés, même si une première version optait pour cette solution qui permettait facilement de changer leur nom en cours d'exécution de programme. En effet, ce genre de manipulation pouvait rendre le système instable et le ralentissait à cause de la multiplication des analyses de listes. En revanche, avec le mécanisme retenu, les primitives sont en interaction avec les objets du noyau grâce à une série de méthodes étudiées plus loin.

V-B-1-c - Le programme de test

Toujours deux versions fournies : une pour Windows et une pour Linux.

Il serait d'ailleurs temps de préciser que seuls les exécutables diffèrent, car le code est strictement le même. Le slogan de Lazarus (« Write once, compile anywhere ») tient ses promesses ! La seule difficulté rencontrée tient à la taille des contrôles : Linux a tendance à prendre plus de place pour l'affichage des étiquettes de TLabel par exemple.

V-B-2 - Les variables globales

V-B-2-a - Définition

Les variables globales sont des variables dont la visibilité est sans limites au sein d'un programme. Par ailleurs, sauf destruction explicite, leur durée de vie est aussi celle de l'exécution du programme.

Leur emploi est déconseillé dans les programmes de grande ampleur, car elles sont sources de confusions possibles.

V-B-2-b - Les variables globales dans GVLOGO

Une variable globale en GVLOGO est définie dans le même temps qu'elle est affectée : DONNE "Ma.Variable [sa valeur] crée la variable Ma.Variable en lui affectant la valeur [sa valeur].

Pendant de la primitive DONNE, CHOSE renvoie la valeur d'une variable si elle existe : ECRIS CHOSE "Ma.Variable. Dans la pratique, on préfèrera utiliser les deux points qui précèdent le nom de la variable pour en récupérer la valeur : ECRIS :Ma.Variable. En cas d'inexistence de la variable, une erreur est générée.

Si une variable locale porte le même nom qu'une variable globale, la première est toujours prioritaire au sein de la procédure où elle est active.

V-B-2-c - L'implémentation des variables globales

Les variables globales font partie intégrante du noyau de GVLOGO.

Voici la partie de l'interface de TGVLogoKernel qui les concerne :

// *** traitement des variables ***
// est-ce une variable?
function IsVar(const Name: string): Boolean;
// nombre de variables
function VarsCount: Integer;
// affectation à une variable
function AddVar(const Name, Value: string): Boolean;
// valeur d'une variable
function ValVar(const Name: string; out Value: string): Boolean; overload;
function ValVar(const Name: string): string; overload;
// destruction d'une variable
function RemoveVar(const Name: string): Boolean;
// destruction d'une liste de variables
function RemoveSomeVars(const Lst: string): Boolean;
// destruction de toutes les variables
function RemoveAllVars: Boolean;
// liste des variables
function VarsToList: string;
// chargement d'un fichier de variables
function LoadVars(const FileName: string): Boolean;
// sauvegarde d'un fichier de variables
function SaveVars(const FileName, Lst: string): Boolean;
// sauvegarde de toutes les variables
function SaveAllVars(const FileName: string): Boolean;

La plupart de ces méthodes font simplement appel aux méthodes génériques en fournissant .VAR comme argument Kind.

V-B-3 - Les procédures

V-B-3-a - Définition

Une procédure est une extension du langage obtenue en agençant des primitives, des données et d'autres procédures afin d'obtenir un résultat particulier.

V-B-3-b - Les procédures dans GVLOGO

Afin de définir une nouvelle procédure, il faut procéder ainsi dans l'éditeur :

- commencer la définition par le mot réservé POUR ;

- faire suivre sur la même ligne ce mot du nom de la nouvelle procédure qui doit être un identificateur correct et non utilisé (sauf pour le nom d'une autre procédure non protégée) ;

- faire suivre, si nécessaire et sur la même ligne, ce nom des noms des paramètres qu'attendra la procédure, chacun étant précédé du signe : et obéissant aussi aux règles de validité des identificateurs ;

- ajouter autant de lignes que voulu au corps de la procédure, c'est-à-dire sa définition proprement dite ;

- terminer la définition par le mot réservé FIN.

Après interprétation de l'éditeur, la nouvelle procédure sera intégrée au langage comme s'il s'agissait d'une primitive. Simplement, contrairement aux primitives, elle pourra être modifiée et supprimée.

Plusieurs primitives permettent de travailler avec les procédures :

.SUP.PROCS supprime la procédure ou la liste de procédures indiquées.

Les résultats peuvent être imprévisibles suivant l'état de l'interpréteur.

CHARGE.PROCS charge le fichier de procédures fourni en paramètre.

COPIE.PROC copie la définition de la procédure (premier paramètre) vers une nouvelle procédure (second paramètre).

DEF.PROC renvoie la définition de la procédure dont le nom a été fourni en paramètre.

EDITE.PROC renvoie vers l'éditeur la définition de la procédure fournie en paramètre.

NB.PARAMS.PROC renvoie le nombre de paramètres de la procédure fournie en paramètre.

PARAMS.PROC renvoie la liste des paramètres de la procédure fournie en paramètre.

PROCEDURE? renvoie une valeur booléenne qui indique si l'objet fourni en paramètre est une procédure ou non.

PROCEDURES renvoie la liste des procédures.

SAUVE.PROCS enregistre la définition des procédures définies dans le fichier dont le nom sera celui fourni par l'unique paramètre.

V-B-3-c - L'implémentation des procédures

La structure interne d'une procédure est plus complexe que celle d'une variable. Voici par exemple la définition d'un carré :

Et sa représentation interne sous forme de liste de propriétés :

On reconnaît la structure de base d'une liste de propriétés avec le nom de la procédure et le nom de propriété .PRC séparé par le signe caractère |. La liste qui suit contient une liste elle-même divisée en deux sous-listes : la première contient les paramètres alors que la seconde est composée de listes qui contiennent chacune une ligne de définition de la procédure. Cette structure est capable d'accueillir d'autres propriétés : par exemple, le fait d'appartenir à un paquet.

Cette relative complexité se retrouve dans les nombreuses méthodes de GVKernel chargées de manipuler les procédures.

Voici la partie interface de TGVKernel concernant les procédures :

// *** traitement des procédures ***
// est-ce une procédure ?
function IsProc(const Name: string): Boolean;
// renvoie le nombre de procédures enregistrées
function ProcsCount: Integer;
// renvoie la liste des procédures
function ProcsToList: string;
// vérifie la validité de la ligne de paramètres
function IsValidParamsLine(Lst: string): Boolean;
// vérifie la validité du corps de la procédure
function IsValidBodyDef(Lst: string): Boolean;
// enregistre une procédure
function AddProc(const Name, Lst: string): Boolean;
// supprime une procédure
function RemoveProc(const Name: string): Boolean;
// destruction d'une liste de procédures
function RemoveSomeProcs(const Lst: string): Boolean;
// supprime toutes les procédures
function RemoveAllProcs: Boolean;
// renvoie le nombre de paramètres d'une procédure
function ParamsCount(const Name: string): Integer;
// renvoie la liste des paramètres
function ParamsLine(const Name: string): string; overload;
function ParamsLine(const Name: string; out ParLine: string)
: Boolean; overload;
// renvoie un paramètre par son numéro
function ParamNum(const Name: string; Num: Integer): string; overload;
function ParamNum(const Name: string; Num: Integer; out ParNum: string)
: Boolean; overload;
// renvoie le nombre de lignes du corps d'une procédure
function ProcLinesCount(const Name: string): Integer;
// renvoie une ligne du corps de la procédure spécifiée
function ProcLine(const Name: string; Line: Integer): string; overload;
function ProcLine(const Name: string; Line: Integer; out PrLine: string)
: Boolean; overload;
// renvoie la définition d'une procédure
function ProcListDef(const Name: string): string; overload;
function ProcListDef(const Name: string; out PrListDef: string)
: Boolean; overload;
// envoie la procédure vers un éditeur
function ProcToEdit(const Name: string; Lst: TStrings): Boolean;
// envoie une liste de procédures vers l'éditeur
function ProcsToEdit(const LstP: string; Lst: TStrings): Boolean;
// envoie toutes les procédures vers l'éditeur
function AllProcsToEdit(Lst: TStrings): Boolean;
// envoie d'un éditeur vers des procédures
function EditToProc(Editor: TStrings; FromLine, ToLine: Integer): Boolean;
// charge des procédures
function LoadProcs(const FileName: string): Boolean;
// sauve des procédures
function SaveProcs(const FileName, Lst: string): Boolean;
// sauvegarde toutes les procédures
function SaveAllProcs(const FileName: string): Boolean;
// définition valide ?
function IsValidDef(const LSt: string): Boolean;
// copie d'une procédure dans une autre
function CopyDef(const FromProc, ToProc: string): Boolean;

Afin de ne pas trop alourdir le document présent, le lecteur est renvoyé au code source largement commenté. Son attention se portera en particulier sur les méthodes chargées de communiquer avec un éditeur via une propriété de type TStrings commeEditToProc ou encore celles qui sont chargées d'analyser une partie de la définition comme ProcListDef :

function TGVLogoKernel.ProcListDef(const Name: string; out PrListDef: string
): Boolean;
// *** liste de définition d'une procédure ***
var
Lst: TGVList;
begin
Result := False;
if IsProc(Name) then // est-ce une procédure ?
begin
Lst := TGVList.Create; // liste de travail
try
Lst.Text := RProp(Name, CProc); // définition
PrListDef := Lst[1]; // recherche de la définition
Result := True; // tout est OK
finally
Lst.Free; // libération de la liste de travail
end;
end
else
// [### Erreur: ce n'est pas une procédure ###]
Error.SetError(CE_UnKnownProc, Name);
end;

Nombreuses sont en effet les méthodes construites autour de ce schéma :

- s'il s'agit d'une procédure :

- construire une liste de travail ;

- effectuer le travail ;

- libérer la liste de travail ;

- sinon signaler une erreur.

V-B-4 - Les paquets

V-B-4-a - Définition

Un paquet est un regroupement d'objets GVLOGO (variables, procédures, listes de propriétés) dont le nom est un identificateur correct et unique. Les paquets permettent de simplifier certaines actions comme les sauvegardes ciblées ou de protéger des objets contre toute modification ou suppression.

V-B-4-b - Les paquets dans GVLOGO

Pour GVLOGO, les paquets sont un type d'objet dont une propriété est .PCK.

La création d'un paquet requiert la primitive PAQUET :

PAQUET "Mon.paquet

Pour un objet isolé, son placement dans un paquet s'opère grâce à la primitive VERS.PAQUET :

VERS.PAQUET "CARRE

Pour une liste d'objets, on utilisera LISTE.VERS.PAQUET.

Un paquet peut être enterré grâce à la primitive ENTERRE : à partir du moment où un paquet est enterré, les objets qui lui appartiennent ne peuvent plus être modifiés, à moins de l'emploi de la primitive DETERRE.

Voici la liste des primitives utilisables pour la gestion des paquets :

APPARTIENT.A renvoie le nom du paquet auquel appartient l'objet indiqué en paramètre ou le mot vide.

DEPAQUETTE permet de sortir l'objet dont le nom est fourni en paramètre du paquet auquel il appartient.

DETERRE rend modifiables les éléments du paquet dont le nom est fourni en paramètre.

EDITE.PAQUET envoie vers l'éditeur l'ensemble des objets appartenant au paquet indiqué par le paramètre.

ELEMENTS.PAQUET renvoie la liste des éléments du paquet dont le nom est fourni en paramètre.

ENTERRE protège le paquet dont le nom est fourni par le premier paramètre.

ENTERRE? indique par une valeur booléenne si l'objet fourni en paramètre est enterré ou non.

LISTE.VERS.PAQUET place dans le paquet indiqué par le premier paramètre l'ensemble des objets contenus dans la liste fournie en second paramètre.

PAQUET crée le paquet dont le nom sera celui fourni en paramètre.

PAQUET.VERS.LISTE renvoie la liste des éléments d'un paquet.

SAUVE.PAQUET sauve les objets du paquet indiqué en paramètre dans un fichier qui portera le nom de ce paquet.

SUPPRIME.PAQUET supprime le paquet indiqué en paramètre, sans supprimer les objets qu'il comprenait.

VERS.PAQUET place l'objet fourni par le second paramètre dans le paquet indiqué par le premier.

V-B-4-c - L'implémentation des paquets

La primitive PAQUET crée la liste de propriétés voulue :

Mon.paquet|[.PKG [VRAI]]

La primitive VERS.PAQUET ajoute la propriété .INP à l'objet placé dans un paquet. Par exemple, si la procédure carre a été placée dans le paquet Mon.paquet, on aura créé :

carre|[.PRC [[[:n] [[// un carré] [polygone 4 :n]]]] .INP [Mon.paquet]]

La procédure carre possède alors deux propriétés : .PRC qui indique qu'il s'agit d'une procédure et .INP qui marque son appartenance au paquet Mon.paquet.

Lorsqu'un paquet est enterré grâce à la primitive ENTERRE, une nouvelle propriété lui est attribuée : .BUR. Par exemple, en imaginant qu'on ait enterré le paquet Mon.paquet :

Mon.paquet|[.PKG [VRAI] .BUR [VRAI]]

Tout objet appartenant à ce paquet est à présent enterré lui aussi, donc non modifiable.

Un objet ne peut appartenir qu'à un unique paquet. Le fait de placer un objet dans un paquet alors qu'il appartenait à un autre, remplace la première affectation, sauf si le paquet est protégé. La propriété .BUR est applicable à tous les objets du paquet, mais n'apparaît que dans la liste représentant ce dernier.

Voici la liste des méthodes qui se rapportent aux paquets :

// *** traitement des paquets ***
// l'objet est-il un paquet ?
function IsPck(const Name: string): Boolean;
// renvoie le nombre de paquets
function PcksCount: Integer;
// le paquet est-il enfoui ?
function IsBurriedPck(const Name: string): Boolean;
// enfouit un paquet
function BurryPck(const Name: string): Boolean;
// déterre un paquet
function UnBurryPck(const Name: string): Boolean;
// empaquette un objet
function ToPck(const Name, Obj: string): Boolean;
// empaquette une liste
function ListToPck(const Name, Lst: string): Boolean;
// renvoie la liste des objets d'un paquet
function PckToList(const Name: string): string;
// renvoie le nombre d'éléments d'un paquet
function CountItemsPck(const Name: string): Integer;
// renvoie la liste des paquets
function PcksToList: string;
// crée d'un paquet
function CreatePck(const Name: string): Boolean;
// détruit un paquet
function RemovePck(const Name: string): Boolean;
// sauvegarde un paquet
function SavePck(const Name: string): Boolean;
// paquet vers éditeur
function PckToEdit(const Name: string; Lst: TStrings): Boolean;
// dépaquette un objet
function UnPackObj(const Name: string): Boolean;
// l'objet appartient-il à un paquet ?
function IsInPck(const Name: string): Boolean;
// l'objet est-il enterré ?
function IsBurried(const Name: string): Boolean;
// à quel paquet appartient un objet ?
function BelongsTo(const Name: string): string; overload;
function BelongsTo(const Name: string; out Which: string): Boolean;
overload;

Ces méthodes ne sont pas très complexes, en dehors du fait qu'il faut tenir compte de la propriété .BUR de protection avant toute modification. Une seconde difficulté naît du fait qu'un objet placé dans un paquet ne connaît que le nom du paquet auquel il appartient et non son état enterré/déterré, pas plus que le paquet ne connaît la liste des objets qu'il comprend : il faut par conséquent balayer la liste des objets dans ce dernier cas et analyser les propriétés du paquet dans le premier.

V-B-5 - Les primitives

V-B-5-a - Définition

Une primitive est un élément connu d'origine par GVLOGO, qui permet de réaliser une action ou de renvoyer une valeur. Les primitives ne sont pas modifiables et leur nombre ne peut pas être étendu depuis GVLOGO.

V-B-5-b - Les primitives dans GVLOGO

GVLOGO comprend plus de 250 primitives à l'heure actuelle. Leur nom est insensible à la casse. En dehors de rares primitives dont le nom est précédé d'un point, elles sont correctement adaptées (on l'espère !) au noyau, à l'affichage, aux messages d'erreurs et au fonctionnement de l'interpréteur. Enfin, on ne peut pas utiliser leur identificateur pour nommer un autre objet.

V-B-5-c - L'implémentation des primitives

Les primitives sont implémentées dans un fichier spécifique Prims.inc qui est inclus dans celui de l'interpréteur. Le noyau comprend cependant un ensemble de méthodes permettant de travailler avec elles :

// *** traitement des primitives ***
// l'objet est-il une primitive ?
function IsPrim(const Name: string): Boolean;
// nombre de primitives
function PrimsCount: Integer; inline;
// numéro de primitive
function NumPrim(const Name: string): Integer;
// nombre de paramètres d'une primitive
function NumParamsPrim(const Name: string): Integer;
// liste des primitives
function PrimsToList: string;
// primitive par numéro
function PrimByNum(const N: Integer): string; overload;
function PrimByNum(const N: Integer; out PrimBNum: string)
: Boolean; overload;

Les commentaires du code source devraient suffire à en comprendre l'objectif. Leur implémentation ne pose pas de problèmes autres que ceux étudiés jusque-là.