Réaliser un interpréteur en Pascal

Avec Free Pascal sous Lazarus

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 : 22 mars 2015

- Mise à jour : 31 mai 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

- I-A. Les objectifs du projet

- I-B. Qu'est-ce que GVLOGO

- I-C. En quoi ce projet peut-il être utile

- I-D. Combien dois-je débourser pour acquérir GVLOGO

- I-E. De quoi ai-je besoin pour travailler

- I-F. Plan du tutoriel

- I-G. Conventions

- I-H. Télécharger GVLOGO

- I-I. Contacter l'auteur

- I-I-1. Remerciements

- II. Les objets de GVLOGO

- II-A. Les fichiers communs

- II-A-1. La centralisation des données

- II-A-2. GVDefines.inc

- II-A-3. GVConsts

- II-A-4. GVErrConsts

- II-A-5. GVPrimConsts

- II-B. Le traitement des erreurs

- II-B-1. L'unité GVErrors

- II-B-2. L'utilisation de l'unité

- II-B-3. Le programme d'exemple

- II-C. Les mots

- II-C-1. Définitions

- II-C-2. Exemples de mots

- II-C-2-a. Mots simples

- II-C-2-b. Mots avec caractère d'échappement

- II-C-2-c. Autres caractères particuliers

- II-C-3. Opérations sur les mots

- II-C-3-a. Fabriquer des mots

- II-C-3-b. Modifier des mots

- II-C-3-c. Extraire depuis des mots

- II-C-3-d. Tester des mots

- II-C-4. Implémentation des mots

- II-C-4-a. L'objet TGVString

- II-C-4-b. La classe TGVNumber

- II-C-4-c. La classe TGVWord

- II-C-4-d. Test de l'unité GVWords

- II-D. Les listes

- II-D-1. Définitions

- II-D-2. Exemples de listes

- II-D-2-a. Listes simples

- II-D-2-b. Listes imbriquées

- II-D-2-c. Listes fautives

- II-D-3. Opérations sur les listes

- II-D-3-a. Fabriquer des listes

- II-D-3-b. Modifier des listes

- II-D-3-c. Extraire de listes

- II-D-3-d. Tester des listes

- II-D-4. Implémentation des listes

- II-D-4-a. Constantes

- II-D-4-b. La classe TGVListUtils

- II-D-4-c. La classe TGVList

- II-D-4-d. Test de l'unité TGVLists

- II-E. Les listes de propriétés

- II-E-1. Définitions

- II-E-2. Exemples de listes de propriétés

- II-E-3. Opérations sur les listes de propriétés

- II-E-4. Implémentation des listes de propriétés

- II-E-4-a. Constantes

- II-E-4-b. La classe TGVPROPLISTENUMERATOR

- II-E-4-c. La classe TGVPROPLIST

- II-E-4-d. Test de l'unité TGVPROPLists

- II-F. La tortue graphique

- II-F-1. Présentation

- II-F-2. Opérations avec la tortue

- II-F-2-a. Champ de la tortue

- II-F-2-b. Déplacements de la tortue

- II-F-2-c. Cap de la tortue

- II-F-2-d. État de la tortue

- II-F-2-e. Le crayon

- II-F-2-f. Formes prédéfinies

- II-F-2-g. Texte avec la tortue

- II-F-3. Implémentation de la tortue

- II-F-3-a. Constantes

- II-F-3-b. Un peu de mathématiques

- II-F-3-c. Se déplacer sur l'écran

- II-F-3-d. Les événements

- II-F-3-e. La classe TGVTURTLE

- II-F-3-f. Test de l'unité GVTurtles

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

- III-A. Le projet EasyTurtle

- III-B. B - Mode d'emploi rapide

- III-B-1. L'écran d'accueil

- III-B-2. La tortue

- III-B-3. Couleurs et formes

- III-B-4. Ordres généraux

- III-B-5. L'aide

- III-B-6. Boîte « À propos »

- III-B-7. Boîte des préférences

- III-B-8. Autres éléments

- III-C. La programmation

- III-C-1. La fiche principale

- III-C-2. Les autres fiches

- III-C-3. Un logiciel tout en français…

- IV. Les outils de programmation

- IV-A. Les piles

- IV-A-1. Définition

- IV-A-2. Exemples

- IV-A-2-a. Un exemple simple

- IV-A-2-b. Un exemple plus complexe

- IV-A-3. Opérations sur les piles (et les queues)

- IV-A-4. Implémentation des piles

- IV-A-4-a. Constantes

- IV-A-4-b. La classe TGVSTack

- IV-A-4-c. Test de l'unité TGVStacks

- IV-B. L'évaluation d'une expression mathématique

- IV-B-1. Définition

- IV-B-2. Opérations dans l'évaluateur

- IV-B-3. Implémentation de l'évaluateur

- IV-B-3-a. Les différentes notations

- IV-B-3-b. L'algorithme shunting-yard (gare de triage)

- IV-B-3-c. La classe TGVEval

- IV-B-3-d. Test de l'unité GVEval

- 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

- VI. L'interpréteur

- VI-A. Principe de fonctionnement

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

- VI-B-1. Les structures utiles

- VI-B-2. DoBegin

- VI-B-3. Process

- VI-B-4. DoEnd

- VI-B-5. DoWord

- VI-B-6. DoList

- VI-B-7. DoVar

- VI-B-8. DoNumber

- VI-B-9. DoEval

- VI-B-10. PushConst

- VI-B-11. DoCommand

- VI-B-12. DoPrim

- VI-B-13. DoProc

- VI-B-14. ExeCommand

- VI-B-15. ExePrim

- VI-B-16. DoExePrim

- VI-B-17. ExeProc

- VI-C. Les primitives

- VI-C-1. Utilitaires

- VI-C-2. La classe TGVAutomatMessage

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

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

- VII. Le programme final

- VII-A. Aperçu de l'organisation du logiciel

- VII-A-1. La coloration syntaxique

- VII-A-2. L'interface utilisateur

- VII-B. Ce qu'il reste à faire

- VII-B-1. L'urgent

- VII-B-2. Le possible et le souhaitable

I - Le projet

Cette partie décrit le projet et fournit des informations générales.

I-A - Les objectifs du projet

Le projet GVLOGO est né en 2014. Il s'agissait d'implémenter le langage LOGO sur un PC (avec Windows ou Linux).

À présent que l'écriture des composants du logiciel est terminée et que ce dernier fonctionne de manière satisfaisante, l'objectif est triple :

- proposer aux programmeurs ou apprentis programmeurs la description de la réalisation complète d'un interpréteur et de son environnement à l'aide d'outils gratuits, évolués, stables et facilement disponibles ;

- offrir un langage de programmation adapté à des enfants, mais dont les possibilités sont étendues grâce au traitement des listes ;

- inviter les nostalgiques des années 70 à des moments de rêverie…

Le langage de programmation choisi pour le développement est un avatar de Pascal qui est connu pour sa grande lisibilité et sa puissance : Free Pascal sous l'environnement de développement intégré Lazarus. Non seulement Pascal a lui aussi des vertus pédagogiques, mais il fonctionne sur de nombreuses plates-formes (dont Windows et Linux) et est gratuit. Les sources de tout le projet sont par ailleurs largement exploitables avec Delphi, un autre environnement plus puissant que le premier, mais payant.

I-B - Qu'est-ce que GVLOGO

GVLOGO est un langage de programmation. Il descend de LOGO, lui-même apparu dans les années 60 à la suite de recherches menées par des universitaires du M.I.T. (U.S.A.) autour du mathématicien Seymour Papert.

S'appuyant sur des travaux de Piaget relatifs à l'acquisition de connaissances et de méthodes chez l'enfant, la visée du projet était pédagogique : rendre l'élève acteur de son apprentissage, y compris et surtout lorsque cet apprentissage devait passer par un ordinateur. En cela le projet s'opposait à l'Enseignement Assisté par Ordinateur alors en vogue qui se contentait le plus souvent d'un scénario figé.

« Dans bien des écoles, aujourd'hui, l'expression « Enseignement Assisté par Ordinateur » signifie que l'ordinateur est programmé pour enseigner à l'enfant. On pourrait dire que l'ordinateur sert à programmer l'enfant. Dans ma vision des choses, l'enfant programme l'ordinateur et, ce faisant, acquiert la maîtrise de l'un des éléments de la technologie la plus moderne et la plus puissante, tout en établissant un contact intime avec certaines des notions les plus profondes de la science, des mathématiques, et de l'art de bâtir des modèles intellectuels. »

(S. Papert - Jaillissement de l'esprit, ordinateurs et apprentissage, Flammarion 1981)

Peut-être avez-vous utilisé ce langage au cours de votre scolarité en pilotant sur l'écran de l'ordinateur un triangle (baptisé « tortue ») grâce à une série de commandes telles que : AVANCE, RECULE, GAUCHE, DROITE… Comme cette tortue pouvait laisser une trace de son passage, vous obteniez des dessins en programmant ses déplacements. Plus tard, peut-être avez-vous aussi manipulé les listes LOGO pour traiter des problèmes d'intelligence artificielle… Et quand bien même vous n'auriez rien fait de tout cela, peu importe, car programmer en LOGO est très simple !

I-C - En quoi ce projet peut-il être utile

Si vous ne connaissez rien à la programmation, GVLOGO (le produit fini) vous initiera à ses grands principes.

Si vous êtes en contact avec des enfants ou des adolescents, GVLOGO leur permettra une première approche de la programmation tout en développant leur sens logique, leur représentation dans l'espace et leur capacité à résoudre des problèmes par eux-mêmes.

Si vous êtes programmeur ou si vous aspirez à l'être, vous pourrez être intéressé par le logiciel lui-même dont les fichiers source sont fournis et largement commentés. Bien qu'il ne s'agisse pas d'un tutoriel d'apprentissage de la programmation, ce projet met en œuvre de nombreuses techniques in vivo. J'ai souvent regretté, en pêchant sur Internet, de rapporter dans mon filet des poissons trop maigres pour me nourrir ou beaucoup trop gros pour mon estomac : entre l'exemple trivial et la bibliothèque absconse, j'ai pensé qu'il devait y avoir l'espace pour un projet moyennement complexe, à portée d'un programmeur solitaire.

I-D - Combien dois-je débourser pour acquérir GVLOGO

Le logiciel est fourni avec tous les fichiers source et exécutables. GVLOGO est protégé par la licence GNU qui garantit le droit de disposer des fichiers fournis sans avoir de redevance à verser à l'auteur. Cependant, ce dernier est protégé quant à l'origine du développement.

La documentation, en particulier le manuel que vous avez devant les yeux, est protégée par un copyright si bien que toute reproduction la concernant, même partielle, doit être soumise à l'auteur pour approbation. Elle est offerte gracieusement avec GVLOGO.

Pour résumer, GVLOGO est entièrement gratuit. Cela dit, si vous jugez ce logiciel utile et que vous mesurez le temps de travail qu'il représente, rien ne vous interdit de verser une contribution, même symbolique, via mon site et mon compte Paypal !

I-E - De quoi ai-je besoin pour travailler

Tous les outils présentés sont entièrement gratuits.

Pour examiner les fichiers source fournis, un simple éditeur suffit : je vous conseille un outil adapté aux développeurs comme NotePad++(1) (pour Windows) ou Geany(2) (pour Linux). Il aura au moins l'avantage de faciliter la lecture des fichiers grâce à la coloration syntaxique.

Pour compiler les mêmes fichiers, Lazarus est le choix qui s'impose puisque GVLOGO a été développé avec lui. Il fonctionne aussi bien sous Windows que sous Linux. La seule bibliothèque extérieure absolument indispensable, elle aussi commune aux deux systèmes d'exploitation, est la bibliothèque graphique BGRABitmap(3) : au TCanvas standard de Free Pascal, elle ajoute entre autres la possibilité de dessiner avec anticrénelage, de contrôler la transparence et de fusionner des couches, ce qui sera très utile au moment d'implémenter la tortue graphique. Le projet final utilise par ailleurs le paquet RichMemo gratuit lui aussi et fourni comme le précédent.

Enfin, les logiciels devraient fonctionner sur les versions de Windows depuis XP et sur Linux. Les tests ont été effectués sans problème avec Windows 8.1 et Linux Mint 17.1 sur un système 64 bits.

I-F - Plan du tutoriel

La réalisation de GVLOGO est modulaire. Tout d'abord, nous créerons une unité de traitement des erreurs qui sera utilisée dans tous les modules. Dans un second temps seront étudiés les objets GVLOGO et leur implémentation : les mots, les listes, les listes de propriétés et la tortue graphique. Les objets sont en quelque sorte les briques qui servent de fondation au langage.

L'étape suivante consistera à coder les outils nécessaires à l'interpréteur, à savoir les piles et un évaluateur d'expressions mathématiques.

Une place importante sera alors consacrée aux variables locales et au noyau. Comme la mission de ce dernier est de gérer le contenu des éditeurs, les variables, les procédures et les paquets, tous ces éléments seront tour à tour étudiés.

Viendra alors l'interpréteur lui-même dont la tâche sera grandement simplifiée par tout le travail réalisé en amont. Pour clore ce qui se rapporte aux fonctionnalités, les primitives seront étudiées avec leur propre unité. Il restera à s'occuper rapidement de l'architecture de l'interface, depuis l'éditeur jusqu'au débogueur, en passant par la zone de saisie, celle de texte et celle réservée à la tortue.

I-G - Conventions

Les éléments de programmation en GVLOGO sont écrits en bleu.

Les éléments de programmation de Pascal, en dehors des listings, sont écrits en rouge. Les classes sont en rouge gras.

Les unités utilisées ou créées sont en noir gras.

Ce pictogramme indique une astuce, un point intéressant pour faciliter la programmation.

Ce pictogramme indique un point à souligner.

Ce pictogramme attire l'attention sur un point sensible, une difficulté particulière.

I-H - Télécharger GVLOGO

Téléchargez GVLOGO ici, avec son code source et ses exemples.

I-I - Contacter l'auteur

Il existe sans doute des méthodes plus efficaces que celles que j'ai employées pour résoudre les problèmes liés à l'implémentation d'un interpréteur LOGO, mais je tenterai de justifier mes choix. Par exemple, l'interpréteur de formules mathématiques a été traité à partir de l'algorithme de « shunting-yard » alors que les solutions courantes font plutôt appel à des arbres : il s'agissait justement de varier les approches et non de reproduire sans cesse les mêmes schémas, car c'est confronté à quelques difficultés que le programmeur, quel que soit son niveau, peut espérer progresser.

Bien entendu, certains aspects du programme sont à améliorer : je pense notamment à la vitesse d'exécution de l'ensemble, même si elle reste tout à fait acceptable. En général, les approches pédagogiques ont été privilégiées, souvent au détriment des performances. Il sera toujours possible de poser des questions, de critiquer les réalisations et de proposer des solutions plus élégantes et/ou plus efficaces que les miennes !

Enfin, l'écriture de milliers de lignes est une aventure à la limite des possibilités humaines : des erreurs émaillent forcément les unités produites. Aussi peu nombreux que possible mais forcément présents, les bogues empoisonnent la vie des utilisateurs et celle des programmeurs : que chacun se fasse un devoir de les signaler et de proposer des solutions pour leur éradication !

Pour cela, n'hésitez pas à envoyer un mail à : gillesvasseur58@gmail.com.

Vous pouvez aussi me joindre via mon site personnel : www.lettresenstock.org.

Bonne lecture, bonne réflexion… et bon LOGO !

Gilles Vasseur, Sunday 22 March 2015.

I-I-1 - Remerciements

Merci à FRANOUCH, f-leb, Cédric DUPREZ, Claude Leloup et Malick SECK pour leur relecture. Je remercie tout particulièrement Alcatîz pour son aide et ses conseils.

II - Les objets de GVLOGO

Cette partie décrit les objets fondamentaux manipulés par GVLOGO. Après un survol des fichiers communs seront successivement abordés le traitement des erreurs, les mots, les listes, les listes de propriétés et la tortue graphique.

II-A - Les fichiers communs

II-A-1 - La centralisation des données

Les avantages d'une centralisation de certaines données sont de deux types :

- il est possible modifier un élément à partir d'un seul point et donc inutile de parcourir tous les fichiers sources éparpillés dans plusieurs unités : on imagine le gain de temps dans la situation où l'on devrait changer un contenu utilisé plusieurs dizaines de fois ;

- le risque d'erreurs difficiles à déceler est moindre puisqu'il n'y a pas à recopier un élément pour l'utiliser ailleurs : coder en dur une chaîne de caractères, par exemple, expose à une copie erronée qui donnera des résultats inattendus lors de comparaisons.

La centralisation des données s'organise autour de quatre fichiers : GVDefines.inc, GVConsts, GVErrConsts et GVPrimConsts. Le projet final GVLOGO utilise par ailleurs un fichier qui lui est propre et sur lequel nous reviendrons en temps utile : GVLOGOConsts.

II-A-2 - GVDefines.inc

Le fichier GVDefines.inc contient les options de compilation. Il permettra par exemple d'optimiser la compilation lors d'une transposition du logiciel vers Delphi. Son contenu s'organise par conséquent autour de directives.

Actuellement, il ne prend en compte que la compilation avec Lazarus, provoquant une erreur si l'on tente d'activer Delphi :

// Lazarus (défini par défaut)
{$DEFINE CompLazarus }

// Delphi prévu... (non défini par défaut)
{.$DEFINE CompDelphi}

// Lazarus : options de compilation
{$IFDEF CompLazarus}
{$UNDEF CompDelphi}
{$MODE objfpc}{$H+}
{$ENDIF}

// Delphi : options de compilation
{$IFDEF CompDelphi}
Implémentation prévue...
{$ENDIF}

// option de débogage (non défini par défaut)
{.$DEFINE Debug}

II-A-3 - GVConsts

L'unité GVConsts servira pour toutes les unités du projet. Elle contient en effet les constantes, types et chaînes de caractères partagés par l'ensemble du projet.

Dans un premier temps, on notera les constantes les plus fréquemment utilisées dans les différentes unités :

const
// *** listes ***
CBlank = ' '; // espace
CTab = #9; // tabulation
CSlash = '/'; // slash
CBeginList = '['; // début de liste
CEndList = ']'; // fin de liste
CBeginPar = '('; // début d'expression
CEndPar = ')'; // fin d'expression
CEmptyList = CBeginList + CEndList; // liste vide

// *** séparateurs ***
CBlanks = [CTab, CBlank];
CBraces = [CBeginList, CEndList, CBeginPar, CEndPar];
CSeparators = CBlanks + CBraces;

// *** caractères ***
CLink = '$'; // caractère de lien
CUnderline = '_'; // soulignement
CDot = '.'; // point
CAsk = '?'; // point d'interrogation
CQuote = '"'; // guillemets
CColon = ':'; // deux points (début de variable)
CComma = ','; // virgule
CExclamation = '!';
CPlus = '+'; // addition
CMinus = '-'; // soustraction
CMul = '*'; // multiplication
CDiv = '/'; // division
CPower = '^'; // puisssance
CGreater = '>'; // plus grand
CLower = '<'; // plus petit
CEqual = '='; // égal
CNotEqual = '<>'; // différent
CNotEqual2 = '!='; // différent (2)
CNot = '!'; // négation
COrB = '|'; // ou binaire
CAndB = '&'; // et binaire
CGreaterOrEqual = '>='; // plus grand ou égal
CLowerOrEqual = '<='; // plus petit ou égal

// *** caractères spéciaux ***
CSpecialChar = [CTab, CBlank, CEqual, CPlus, CMinus, CMul, CDiv, CBeginPar,
CEndPar, CPower, CGreater, CLower];
CNonPrintable = [#0..#32];

// *** ensembles de caractères courants ***
CLowAlpha = ['a' .. 'z']; // caractères alphabétiques en minuscules
CHighAlpha = ['A' .. 'Z']; // caractères alphabétiques en majuscules
CAlpha = CLowAlpha + CHighAlpha; // caractères alphabétiques
// caractères alphabétiques autorisés pour un identificateur
CAlphaPlus = CAlpha + [CUnderline, CColon, CDot];
CDigit = ['0' .. '9']; // chiffres
CDigitPlus = CDigit + [CPlus, CMinus]; // chiffres et signes
CDigitPlusDot = CDigitPlus + [CDot]; // point en plus
CAlphaNum = CAlpha + CDigit;  // caractères alphanumériques
// caractères autorisés pour un identificateur
CAlphaNumPlus = CAlphaPlus + CDigit;
// caractères autorisés hors chaîne de caractères
CAllValidChars = CAlphaNumPlus + CSpecialChar;

// *** listes de propriétés ***
CExtPl = '.GPL'; // extension pour les fichiers de listes de propriétés
// entête de fichier de listes de propriétés
CHeader = '[GPL100 (c) GV 2014]';
CSep = '|'; // séparateur de liste de propriétés

// *** valeurs remarquables ***
CRLower = -1; // résultats de comparaison (plus petit)
CREqual = 0; // résultats de comparaison (égal)
CRGreater = 1; // résultats de comparaison (plus grand)
CRTrue = -1; // vrai
CStTrue = '-1';
CRFalse = 0; // faux
CStFalse = '0';

// *** tortue graphique ***
// couleurs de base
CColors: array[0..19] of TColor = (clBlack, clMaroon, clGreen, clOlive,
clNavy, clPurple, clTeal, clGray, clSilver, clRed, clLime, clYellow,
clBlue, clFuchsia, clAqua, clWhite, clMoneyGreen, clSkyBlue, clCream,
clMedGray);
DgToRad = Pi / 180; // pour les conversions en radians
RadToDg = 180 / Pi; // pour les conversions en degrés
CDefaultScale = 100; // échelle par défaut
CDefaultHeading = 90; // cap par défaut
CDefaultXY = 600; // taille écran tortue par défaut
CDefaultSize = 8; // taille d'une tortue par défaut
CMaxSize = 20; // taille maximale de la tortue
CMaxSpeed = 100; // vitesse maximum de la tortue
CDefaultPenColor = clWhite; // couleur de fond par défaut
CDefaultBackColor = clBlack; // couleur du crayon par défaut
CDefaultPenWidth = 1; // largeur du crayon par défaut

// *** piles ***
CMinStack = 8; // minimum d'espace pour une pile

// *** 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

// *** interpréteur ***
CDisabledState = 1; // désactivé
CTrueState = -1; // vrai
CFalseState = 0; // faux
CBreak = '#'; // marque de fin

Les autres éléments de cette unité seront étudiés lors de la description des unités qui y font appel.

II-A-4 - GVErrConsts

L'unité GVErrConsts abrite les éléments de base utiles pour la gestion des erreurs. Elle comprend les chaînes des messages, un type énumération pour les erreurs possibles et un tableau reprenant ces messages. Un enregistrement décrit une erreur :

// *** enregistrement d'une erreur ***
TGVErrorRec = record
Code: TGVError; // code de l'erreur
ErrItem: string; // élément fautif dans la ligne de travail
ErrPos: Integer; // position (absolue ou relative) de l'élément fautif
end;

II-A-5 - GVPrimConsts

L'unité GVPrimConsts fait de même, mais pour ce qui concerne les primitives du langage GVLOGO, c'est-à-dire les mots qui renvoient à une action/commande comprise d'origine par le langage.

L'enregistrement d'une primitive comprend son nom et le nombre de paramètres qu'elle attend :

type
// *** enregistrement d'une primitive ***
TGVPrimRec = record
Name: string; // nom
NbParams: Integer; // nombre de paramètres
end;

L'unité contient alors la liste des noms possibles et un tableau des enregistrements des primitives.

On remarquera que la partie implémentation de ces unités est vide, le rôle de ces dernières étant seulement de déclarer des types, des chaînes et des constantes.

L'utilisation de resourcestring facilitera la traduction du logiciel en une autre langue. Toutefois, l'utilisation de tableaux fait que les chaînes traitées selon ce mode ne seront pas traduites automatiquement(4).

II-B - Le traitement des erreurs

Il peut paraître surprenant de commencer par la gestion des erreurs, mais la réalisation d'une unité utilisée par tous les modules nécessaires au projet permettra de simplifier leur écriture. L'objectif est de fournir un outil simple de centralisation des erreurs : à partir d'un minimum de déclarations, tout module gèrera de manière homogène une éventuelle erreur.

II-B-1 - L'unité GVErrors

L'unité GVErrors concentre les outils nécessaires à cette gestion des erreurs. L'élément fondamental de communication est le gestionnaire d'événements relatifs aux erreurs :

type
// *** notification d'une erreur ***
TGVOnErrorEvent = procedure(Sender: TObject; ErrorRec: TGVErrorRec) of object;

Comme tout gestionnaire d'événements, il propose un modèle de procédure (ou, plus rarement, de fonction) avec une signature particulière (ici, un Sender de type TObject et un enregistrement d'erreur ErrorRec de type TGVErrorRec tel que défini plus haut dans l'unité GVErrConsts).

Voici l'interface de la classe TGVErrors définie dans GVErrors :

TGVErrors = class(TObject)
strict private
fOnError: TGVOnErrorEvent; // notification de changement
fErrorRec: TGVErrorRec; // enregistrement en cours
fErrorMessage: string; // message d'erreur en cours
fOKFlag: Boolean; // drapeau d'erreur
function GetOKFlag: Boolean; // récupération du drapeau d'erreur
procedure SetOKFlag(AValue: Boolean); // mise à jour du drapeau d'erreur
protected
// changement notifié
procedure NotifyError;
public
constructor Create; // constructeur
destructor Destroy; override; // destructeur
procedure Clear; // nettoyage des erreurs
// procédure concernant la collecte des informations
procedure SetError(const Code: TGVError; ErrItem: string; ErrPos:
Integer = CE_NoErr); virtual;
// centralisation des informations
procedure GetError(Sender: TObject; ErrorRec: TGVErrorRec);
// message associé à une erreur
property ErrorMessage: string read fErrorMessage;
// notification de changement
property OnError: TGVOnErrorEvent read fOnError write fOnError;
// drapeau d'erreur
property Ok: Boolean read GetOKFlag write SetOKFlag default True;
// enregistrement d'erreur
property Error: TGVErrorRec read fErrorRec write fErrorRec;
end;

ErrorMessage transforme le numéro d'une erreur en un message lisible. La propriété Error permet d'accéder à l'enregistrement de l'erreur. La propriété OK permet de tester si une erreur a été rencontrée ou non.

Plus intéressant est le gestionnaire OnError qui sera activé à chaque erreur rencontrée. C'est par son intermédiaire que les erreurs seront traitées par l'unité appelante. SetError et GetError permettent de gérer les données associées aux erreurs. La méthode GetError a une signature compatible avec le type TGVOnErrorEvent si bien qu'elle peut récupérer les erreurs d'une unité qui gérerait elle-même un objet de type TGVErrors !

Voici par exemple comment l'interpréteur récupère et centralise les erreurs détectées dans les objets qu'il manipule :

constructor TGVAutomat.Create;
// *** création ***
begin
// piles
fWkStack := TGVStringStack.Create; // travail
fParamsStack := TGVIntegerStack.Create; // paramètres
fDatasStack := TGVStringStack.Create; // données
fCommandsStack := TGVStringStack.Create; // commandes
fExeStack := TGVStringStack.Create; // exécution
// drapeaux
fStop := False; // pas d'arrêt
fFollow := False; // pas de suivi
fTurtleOutPut:= False; // écriture sur l'écran normal
fState := asWaiting; // en attente
// modules
Error := TGVErrors.Create; // traitement des erreurs
fKernel:= TGVLogoKernel.Create; // noyau
fKernel.Error.OnError := @Error.GetError; // gestionnaire centralisé d'erreurs
fLocVars := TGVLocVars.Create; // variables locales
fLocVars.Error.OnError := @Error.GetError; // gestionnaire centralisé d'erreurs
fEval := TGVEval.Create; // évaluateur
fEval.Kernel := fKernel; // noyau et évaluateur liés
fEval.LocVars := fLocVars; // idem pour les variables locales
fEval.Error.OnError := @Error.GetError; // gestionnaire centralisé d'erreurs
fMessage := TGVAutomatMessage.Create; // messages
end;

Une fois assuré le traitement local des erreurs par la création de l'objet Error (instance de la classe TGVErrors), les objets fKernel, fLocVars et fEval voient leurs gestionnaires d'erreurs reliés à celui de leur hôte. À présent, toute erreur déclenchée dans leur sein sera répercutée dans Error de l'interpréteur.

II-B-2 - L'utilisation de l'unité

Le processus de déclaration est toujours le même. Voici, par exemple, la classe TGVNumber qui traite les nombres :

// *** classe des nombres ***
TGVNumber = class(TObject)
strict private
fError: TGVErrors; // traitement des erreurs
fNum: Double; // nombre de travail
fSt: string; // chaîne brute d'entrée
fValid: Boolean; // drapeau de validité
function GetInt: Integer; // renvoie un entier
function GetDouble: Double; // renvoie un réel
function GetStr: string; // acquiert une chaîne à convertir en nombre
procedure SetStr(const St: string); // forme une chaîne à partir d'un nombre
public
constructor Create; // constructeur
destructor Destroy; override; // destructeur
procedure Clear; // remise à zéro
function IsValid: Boolean; // est-ce un nombre ?
function IsInt: Boolean; // est-ce un entier ?
function IsZero: Boolean; // nombre 0 ?
function IsNegate: Boolean; // nombre négatif ?
property Text: string read GetStr write SetStr; // une chaîne de nombre
property AsDouble: Double read GetDouble; // un réel
property AsInt: Integer read GetInt; // un entier
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

On crée une propriété qui renvoie à une variable privée. Dans le constructeur Create, on crée l'instance de classe tandis qu'on la libère dans le destructeur Destroy. Il suffit à présent de générer quand nécessaire une erreur pour qu'elle puisse être exploitée : on utilisera la méthode SetError qui requiert un numéro d'erreur, le nom de l'élément ayant déclenché l'erreur et facultativement un emplacement de l'erreur dans l'élément.

Dans le programme appelant, on crée une méthode d'affichage des erreurs :

procedure GetError(Sender: TObject; ErrorRec: TGVErrorRec);

Voici le listing de cette méthode qui affiche les données associées à l'erreur :

procedure TMainForm.GetError(Sender: TObject; ErrorRec: TGVErrorRec);
// gestionnaire d'erreurs
begin
// message en toutes lettres
mmoMain.Lines.Add('// >>> ' + (Sender as TGVErrors).ErrorMessage);
with mmoMain.Lines, ErrorRec do
begin
Add('// Code: ' + IntToStr(Ord(Code))); // code de l'erreur
Add('// Elément : ' + ErrItem); // élément fautif dans la ligne de travail
if ErrPos <> CE_NoErr then // position pertinente ?
Add('// Position : ' + IntToStr(ErrPos)); // position de l'erreur
Add('');
end;
end;

Lorsqu'on crée la fiche de travail, on relie cette méthode à l'unité de gestion des erreurs :

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;

On remarquera que le même gestionnaire est ici partagé par deux unités : c'est tout l'intérêt de notre centralisation des erreurs que de n'avoir qu'une seule méthode de traitement à coder.

À présent, tout ce qu'il reste à faire, c'est de vérifier l'état d'erreur de l'unité. Si une erreur a été levée, elle a été traitée.

Il n'est par conséquent pas besoin de tester quoi que ce soit d'autre :

procedure TGVMainForm.btnFirstClick(Sender: TObject);
// test de FIRST
var
LS: string;
begin
fWord.Text := LabEdtFirst.Text; // on affecte
LS := fWord.First; // on effectue l'opération
if fWord.Error.OK then // pas d'erreur ?
lblResult.Caption := LS; // on affiche
end;

Dans la méthode donnée en exemple, on récupère le contenu d'un composant TLabelEdit qu'on transforme en mot formaté. Le second mot issu lui aussi d'un TLabelEdit est comparé au premier. S'il n'y a pas eu d'erreur, on affiche le résultat, à savoir le mot qui précède l'autre par ordre alphabétique. En cas d'erreur, un message aura automatiquement été affiché.

II-B-3 - Le programme d'exemple

Les versions proposées pour le programme d'exemple sont au nombre de deux :

- une version Lazarus Win32 ;

- une version Lazarus Linux.

On trouvera les fichiers sources et les exécutables dans le sous-dossier « 00 - testgverrors » du dossier « tests ».

Toutes les unités produites seront ainsi testées par un programme autonome. Cette démarche permet d'isoler les éventuels problèmes : il sera bien plus facile de repérer une erreur dans un module que dans le programme final qui comportera des milliers de lignes !

En dehors de la gestion des erreurs déjà étudiée, le programme est tout à fait trivial. Un composant TMemo affiche les messages d'erreurs. Chacune d'entre elles est générée de manière aléatoire.

II-C - Les mots

II-C-1 - Définitions

GVLOGO travaille essentiellement à partir de mots et de listes. Ce chapitre se propose d'étudier les mots.

Un mot est une suite quelconque de caractères.

Un mot est délimité par un espace, les signes [, ], (, ). Ces cinq caractères particuliers sont appelés des délimiteurs : ils indiquent à GVLOGO comment séparer les éléments du langage.

Afin d'inclure un délimiteur dans un mot, on le fait précéder d'un autre caractère appelé caractère d'échappement : c'est le $. Si ce caractère d'échappement doit apparaître en tant que tel dans un mot, il faut le faire précéder de lui-même !

Il existe un mot particulier qui ne comprend aucun caractère : on l'appelle le mot vide. On le représente par un guillemet anglais suivi d'un espace.

Les mots "VRAI et "FAUX, qui peuvent s'écrire simplement VRAI et FAUX, sont réservés au renvoi de valeurs booléennes. Pour GVLOGO, la valeur « vrai » est représentée par le nombre -1 alors que la valeur « faux » l'est par le nombre 0.

II-C-2 - Exemples de mots

II-C-2-a - Mots simples

Voici des mots simples qui ne poseront pas de problèmes particuliers :

- libellule

- porte-avions

- entendre

- EnTenDRe

Les nombres ne sont que des mots particuliers qui seront traités comme des mots ordinaires ou comme des nombres, suivant l'opération en cours :

- 1245

- 1E2(5)

- 13456,58

Les mots accentués et les caractères exotiques sont utilisables eux aussi, à condition de ne pas être choisis en tant qu'identificateur :

- éléphant

- %&/ç\

Les caractères particuliers comme ceux qui sont accentués ne sont pas toujours bien gérés par les langages de programmation et les systèmes d'exploitation. Les normes se sont multipliées et des hiéroglyphes étranges peuvent parfois apparaître. Le travail de développement de Lazarus est largement déterminé par ce problème. Nous y reviendrons lorsque nous aborderons l'implémentation des mots. Disons que, pour le moins, il s'agit d'un point délicat susceptible de troubler le programmeur.

II-C-2-b - Mots avec caractère d'échappement

Un caractère pourtant interdit dans un mot GVLOGO peut toujours être introduit s'il est précédé du caractère d'échappement :

- Victor$ Hugo

- GVLOGO$ est$ un$ langage$ facile$ à$ apprendre

- $[encore$ un$ mot$]

- $(un$ autre$)

- 15$ $$

Lors de l'écriture des mots ainsi construits, le caractère d'échappement, sauf contre-ordre, n'apparaîtra pas à l'écran :

- ECRIS $[encore$ un$ mot$] donnera [encore un mot]

- ECRIS 15$ $$ donnera 15 $

Dès qu'une suite de mots est en jeu, l'emploi des listes est préconisé. Le caractère d'échappement est une commodité, sans plus. Il est plus facile d'écrire et de comprendre ECRIS [15 $] que ECRIS 15$ $$.

II-C-2-c - Autres caractères particuliers

En plus des délimiteurs, certains caractères sont utilisés par le langage lui-même et déterminent son comportement selon le contexte :

- le " (guillemet anglais) indique que le mot qui suit doit être pris tel quel, sans chercher à l'interpréter (par exemple, ECRIS "Bonjour.) ;

- les : (deux-points) indiquent que le mot qui suit est une variable dont la valeur est à rechercher (par exemple, ECRIS :salutation affichera, si elle existe, la valeur qui correspond à la variable « salutation ») ;

- le . (point) en début de mot indique une primitive dont l'emploi est risqué, car elle touche au cœur de GVLOGO (par exemple .EFFACE.TOUT détruit tous les objets en réinitialisant le noyau) ;

- le ? (point d'interrogation) est utilisé à la fin des primitives qui renvoient une valeur booléenne (VRAI/FAUX), mais, comme le point, son utilisation est une question d'habitude et n'a pas de caractère obligatoire.

II-C-3 - Opérations sur les mots

Ci-après, le lecteur trouvera les primitives connues de GVLOGO qui concernent les mots. Pour chaque primitive, avant sa définition, il est indiqué le nombre de paramètres attendus, leurs types et le type de valeur qu'elle renvoie. Au moins un exemple illustre l'emploi de la primitive décrite.

Les primitives énumérées peuvent évidemment se combiner pour obtenir le résultat escompté : c'est même ainsi qu'elles prennent tout leur intérêt ! On y reviendra dans la partie qui traitera de la réalisation de programmes en GVLOGO.

II-C-3-a - Fabriquer des mots

METS.PREMIER (raccourci : MP) : attend deux mots en entrée - renvoie un mot - le mot rendu est composé du premier paramètre placé avant le second.

Exemple :

- ECRIS METS.PREMIER "tourne "dos → tournedos

METS.DERNIER (raccourci : MD) : attend deux mots en entrée - renvoie un mot - le mot rendu est composé du premier paramètre placé après le second.

Exemple :

- ECRIS METS.DERNIER "tourne "dé → détourne

MOT : attend deux mots en entrée - renvoie un mot - le mot rendu est composé du second paramètre placé après le premier.

Exemple :

- ECRIS MOT "dé "tourne → détourne

Il s'agit donc d'un synonyme de METS.DERNIER. Cependant, cette dernière primitive fonctionne aussi avec les listes alors que MOT déclenchera une erreur si une liste est fournie en entrée.

II-C-3-b - Modifier des mots

INSERE : attend un entier suivi deux mots en entrée - renvoie un mot - Le mot rendu est composé du deuxième paramètre inséré dans le dernier paramètre à la position précisée par l'entier.

Exemple :

- ECRIS INSERE 4 "en "atttion → attention

INVERSE : attend un mot en entrée - renvoie un mot - le mot rendu est celui d'entrée dont les lettres ont été inversées.

Exemple :

- ECRIS INVERSE "billet → tellib

MAJUSCULES : attend un mot en entrée - renvoie un mot - le mot rendu est en majuscules.

Exemple :

- ECRIS MAJUCULES "éléphant → ĒLĒPHANT

MELANGE : attend un mot en entrée - renvoie un mot - le mot rendu a les mêmes lettres que celui d'origine, mais dans un ordre aléatoire.

Exemple :

- ECRIS MELANGE "alouette → eoetault

MINUSCULES : attend un mot en entrée - renvoie un mot - le mot rendu est en minuscules.

Exemple :

- ECRIS MINUSCULES "ESSAI → essai

REMPLACE : attend un entier suivi de deux mots en entrée - renvoie un mot - le mot rendu est composé de toutes les lettres du second mot en entrée, sauf celle visée par l'entier qui a été remplacée par le premier mot.

Exemple :

- ECRIS REMPLACE 3 "é "éliment → élément

TRIE : attend un mot en entrée - renvoie un mot - les lettres du mot sont triées par ordre alphabétique(6).

Exemple :

- ECRIS TRIE "important → aimnoprtt

ROTATION : attend un mot en entrée - renvoie un mot - la primitive renvoie le mot dont le premier caractère est placé à la fin.

Exemples :

- ECRIS ROTATION "essai → ssaie

II-C-3-c - Extraire depuis des mots

ELEMENT : attend un entier suivi d'un mot en entrée - renvoie un mot - le mot rendu est composé de la lettre du mot en entrée indiquée par l'entier fourni.

Exemple :

- ECRIS ELEMENT 4 "Zorro → r

DERNIER (raccourci : DER) : attend un mot en entrée - renvoie un mot - le mot renvoyé est composé de la dernière lettre du mot en entrée.

Exemple :

- ECRIS DERNIER "important → t

HASARD : attend un mot en entrée - renvoie un mot - le mot renvoyé est composé d'une lettre du mot en entrée, tirée au hasard. Si le mot en entrée est un nombre, le mot renvoyé est un nombre compris entre 1 et ce nombre.

Exemples :

- ECRIS HASARD "destin → i

- ECRIS HASARD "destin → d

- ECRIS HASARD "10 → 8

PREMIER (raccourci : PREM) : attend un mot en entrée - renvoie un mot - le mot renvoyé est composé de la première lettre du mot en entrée.

Exemple :

- ECRIS PREMIER "important → i

SAUF.DERNIER (raccourci : SD) : attend un mot en entrée - renvoie un mot - le mot renvoyé est le mot en entrée amputé de sa dernière lettre.

Exemple :

- ECRIS SAUF.DERNIER "important → importan

SAUF.PREMIER (raccourci : SP) : attend un mot en entrée - renvoie un mot - le mot renvoyé est le mot en entrée amputé de sa première lettre.

Exemple :

- ECRIS SAUF.PREMIER "important → mportant

II-C-3-d - Tester des mots

PLG? : attend deux mots en entrée - renvoie un booléen - la primitive renvoie "VRAI si le premier mot vient strictement après le second selon l'ordre alphabétique, "FAUX sinon.

Exemples :

- ECRIS PLG? "petit "grand → -1

- ECRIS PLG? 1245 138 → -1

Comme EGAL? et PLP?, PLG? se comporte différemment si les deux mots sont des nombres : ce sont leurs valeurs qui sont alors comparées.

PLP? : attend deux mots en entrée - renvoie un booléen - la primitive renvoie "VRAI si le premier mot vient strictement avant le second selon l'ordre alphabétique, "FAUX sinon.

Exemple :

- ECRIS PLP? "petit "grand → 0

COMPTE : attend un mot en entrée - renvoie un entier - la primitive renvoie le nombre de caractères du mot en entrée.

Exemple :

- ECRIS COMPTE "urticaire → 9

EGAL? : attend deux mots en entrée - renvoie un booléen - la primitive renvoie "VRAI si le premier mot est égal au second selon l'ordre alphabétique, "FAUX sinon.

Exemple :

- ECRIS EGAL? "100 "1E2 → -1

IDENTIFICATEUR? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est un identificateur correct, "FAUX sinon.

Exemples :

- ECRIS IDENTIFICATEUR? "MaFonction12 → -1

- ECRIS IDENTIFICATEUR? "12MaFonction → 0

Un identificateur est correct quand il ne comprend que des lettres non accentuées, des chiffres non placés en première position, le signe _ (souligné) et le signe . (point) en début de mot, ou le ? (point d'interrogation) à la fin.

MEMBRE? : attend deux mots en entrée - renvoie un booléen - la primitive renvoie "VRAI si le premier mot est compris dans le second, "FAUX sinon.

Exemple :

- ECRIS MEMBRE? "an "étonnant → -1

MOT? : attend un objet en entrée - renvoie un booléen - la primitive renvoie "VRAI si l'objet est un mot, "FAUX sinon.

Exemple :

- ECRIS MOT? [coucou] → 0

NOMBRE? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot est un nombre, "FAUX sinon.

Exemple :

- ECRIS NOMBRE? "12458 → -1

Le nombre est ici précédé d'un guillemet anglais, comme le serait un mot ordinaire. Pour les nombres, cette écriture est facultative.

PRECEDENT : attend deux mots en entrée - renvoie un mot - la primitive renvoie le mot qui vient le premier selon l'ordre alphabétique.

Exemple :

- ECRIS PRECEDENT "un "deux → deux

SUIVANT : attend deux mots en entrée - renvoie un mot - la primitive renvoie le mot qui vient le dernier selon l'ordre alphabétique.

Exemple :

- ECRIS SUIVANT "un "deux → un

VIDE? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est le mot vide, "FAUX sinon.

Exemple :

- ECRIS VIDE? " → -1

II-C-4 - Implémentation des mots

L'implémentation des mots passe par l'unité GVConsts qui contient les constantes centralisées du projet et une unité baptisée GVWords qui abrite les classes nécessaires à la gestion proprement dite des mots.

II-C-4-a - L'objet TGVString

La difficulté essentielle dans le traitement des mots en GVLOGO réside dans la présence d'un caractère d'échappement dont il faut tenir compte pour toutes les opérations portant sur eux. L'objet TGVString accomplit le travail de normaliser tout mot qui lui est proposé. Il est capable de restituer le mot d'origine ainsi que celui transformé.

Voici sa partie interface :

type
// *** objet chaîne ***
TGVString = object
strict private
fRawStr: string; // chaîne brute entrée
fIsValid: Boolean; // validité de la chaîne d'entrée
fStr: string; // chaîne brute interne
fFmtStr: string; // chaîne formatée interne
function GetFmtStr: string; // renvoie la chaîne formatée
function GetStr: string; // renvoie la chaîne brute
procedure SetStr(const St: string); // établit une nouvelle chaîne brute
function WithEsc(const St: string): string; // avec échappement
function WithoutEsc(const St: string): string; // sans échappement
public
procedure Clear; // remise à zéro
property IsValid: Boolean read fIsValid; // validité de la chaîne d'entrée
property Str: string read GetStr write SetStr; // chaîne brute d'entrée
property FmtStr: string read GetFmtStr; // chaîne formatée
property RawStr: string read fRawStr; // chaîne brute
end;

L'interface est bâtie de manière « classique » en proposant des propriétés dont l'implémentation est protégée par des méthodes privées. La propriété Str lit une chaîne afin de la transformer en chaîne normalisée et de la restituer ainsi. La propriété en lecture seule RawStr permet de retrouver la chaîne brute, telle qu'elle a été introduite.

La méthode SetStr est chargée de transformer la chaîne fournie en entrée. Elle invoque successivement WithoutEsc qui retire un éventuel formatage et WithEsc qui normalise la chaîne. Cette façon de procéder évite de formater une chaîne qui le serait déjà : en revanche, elle exige deux analyses de la chaîne et ne repère pas les mots normalisés par hasard !

Un objet a été préféré à une classe dans le cas bien précis où aucun appel à un constructeur et/ou à un destructeur n'était nécessaire. En effet, l'objet n'utilise pas de méthodes virtuelles et n'utilise aucune classe particulière.

Encore un avantage à déléguer un travail particulier à un objet ou à une classe : un meilleur algorithme pourra être implémenté dans cette classe sans avoir à modifier la moindre ligne des autres fichiers. Autrement dit, une fois l'interface bien définie, il n'importe en rien aux classes qui s'en servent de savoir comment le problème est traité : seul le résultat compte. Il est même très judicieux d'imposer qu'un objet ou une classe n'ait aucun présupposé à faire avant d'utiliser une autre classe.

L'algorithme utilisé par WithEsc est trivial. On remarquera cependant le couple for…in qui balaie la chaîne en lieu et place d'une boucle traditionnelle for…next moins lisible :

function TGVString.WithEsc(const St: string): string;
// *** normalise un mot en tenant compte du caractère d'échappement ***
var
Lch: Char;
begin
Result := EmptyStr; // chaîne vide par défaut
for Lch in St do // balaie la chaîne de départ
begin
if Lch in CSeparators + [CLink] then
// si séparateur ou échappement suit, on insère un échappement
Result := Result + CLink;
Result := Result + Lch; // caractère en cours ajouté
end;
end;

Les utilisateurs de Delphi devront remplacer in CSeparators par la fonction CharInSet s'ils ne veulent pas obtenir un message d'avertissement.

La méthode WithoutEsc n'intéressera que pour l'utilisation d'un drapeau (variable LFlag) qui mémorise le fait d'avoir rencontré précédemment un caractère d'échappement. Ce drapeau est remis à zéro lorsqu'on a traité son repérage :

function TGVString.WithoutEsc(const St: string): string;
// *** sans caractère d'échappement ***
var
LCh: Char;
LFlag: Boolean;
begin
Result := EmptyStr;
// chaîne vide par défaut
LFlag := False; // on n'a pas eu affaire à un caractère d'échappement
for LCh in St do // on balaie la chaîne
if (LCh <> CLink) or LFlag then // caractère d'échappement initial ?
begin
Result := Result + LCh; // non : on ajoute simplement
LFlag := False; // on indique ce cas
end
else
LFlag := True; // échappement initial qu'on ignore
end;

Les utilisateurs de Delphi pourront envisager de transformer cet objet en enregistrement. En effet, cet EDI accepte des enregistrements comprenant des méthodes.

II-C-4-b - La classe TGVNumber

La classe TGVNumber traite les chaînes susceptibles de représenter des nombres.

Voici sa partie interface :

TGVNumber = class(TObject)
strict private
fError: TGVErrors; // traitement des erreurs
fNum: Double; // nombre de travail
fSt: string; // chaîne brute d'entrée
fValid: Boolean; // drapeau de validité
function GetInt: Integer; // renvoie un entier
function GetDouble: Double; // renvoie un réel
function GetStr: string; // acquiert une chaîne à convertir en nombre
procedure SetStr(const St: string); // forme une chaîne à partir d'un nombre
public
constructor Create; // constructeur
destructor Destroy; override; // destructeur
procedure Clear; // remise à zéro
function IsValid: Boolean; // est-ce un nombre ?
function IsInt: Boolean; // est-ce un entier ?
function IsZero: Boolean; // nombre 0 ?
function IsNegate: Boolean; // nombre négatif ?
property Text: string read GetStr write SetStr; // une chaîne de nombre
property AsDouble: Double read GetDouble; // un réel
property AsInt: Integer read GetInt; // un entier
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

La partie publique comprend, outre le constructeur et le destructeur, une procédure Clear qui remet à zéro le nombre interne, quatre fonctions utiles et quatre propriétés.

L'entrée dans la classe se fait par la propriété AsString. Si possible, la chaîne est de manière interne transformée en nombre. En cas d'échec, la fonction IsValid retournera False comme résultat. L'accès en lecture d'un nombre erroné déclenchera une erreur.

La lecture peut être effectuée de trois manières : sous forme de chaîne (AsString), d'entier (AsInt) ou de nombre réel (AsDouble). Il est par ailleurs possible de tester le nombre interne pour savoir s'il s'agit d'un entier grâce à la fonction IsInt. La fonction IsNegate permet de savoir si le nombre est négatif. Enfin, la fonction IsZero compare de manière correcte des nombres réels qui seraient jugés inégaux pour des raisons de représentation interne.

L'implémentation ne pose pas de problèmes particuliers. Voici celle de SetStr qui gère l'entrée d'une nouvelle chaîne à traduire :

procedure TGVNumber.SetStr(const St: string);
// *** forme un nombre si possible ***
begin
fSt := St; // chaîne brute affectée
fValid := TryStrToFloat(St, fNum); // transformation possible ?
end;

On conserve le paramètre d'entrée et on le teste grâce à la fonction TryStrToFloat. Le drapeau privé fValid sera vérifié avant de renvoyer une valeur : l'utilisateur est ainsi assuré de toujours disposer d'une valeur correcte ou d'obtenir le déclenchement d'une erreur.

En règle générale, il faut éviter de court-circuiter les erreurs et toujours avertir l'utilisateur que la valeur dont il dispose est erronée. En croyant bien faire pour l'utilisateur final, on le prive dans le cas contraire de tout repère sur la stabilité de son système et sur la confiance qu'il peut accorder aux résultats qu'il obtient.

II-C-4-c - La classe TGVWord

La classe TGVWord est de loin la plus fournie de cette unité. Elle gère les mots, qu'ils soient des chaînes de caractères ou des nombres, en reprenant toutes les fonctionnalités définies dans la description du langage GVLOGO qui concernent les mots.

La classe utilise deux champs privés de la classe TGVString et autant de la classe TGVNumber : ils servent d'outils de travail et de comparaisons. L'emploi de ces classes évite d'avoir constamment à vérifier la présence des caractères spéciaux.

En fait, la difficulté essentielle tient aux lacunes de Lazarus dans la gestion des caractères accentués. Les fonctions préfixées par UTF8 présentes dans l'unité lazutf8 constituent une solution possible qui règle la plupart des problèmes, excepté ceux relatifs à l'ordre alphabétique : ainsi, la fonction Sort ne triera pas correctement les lettres d'un mot, rejetant les caractères accentués après les caractères normaux. Delphi ne présente pas ce problème. On notera par ailleurs que la fonction Sort utilise un tri à bulles bien suffisant pour des quantités très limitées d'objets à trier.

En voici l'interface :

TGVWord = class(TObject)
strict private
fError: TGVErrors; // traitement des erreurs
fText: TGVString; // mot à traiter
fNum, fNum2: TGVNumber; // nombres de travail
function GetFmtText: string; // texte formaté
function GetText: string; // texte brut
procedure SetText(const St: string); // établit le texte
protected
function Compare(const St: string): Integer; // comparaison de deux mots
public
constructor Create; // constructeur
destructor Destroy; override; // destructeur
procedure Clear; // nettoyage
function First: string; // premier caractère d'un mot
function Last: string; // dernier caractère d'un mot
function ButFirst: string; // sauf le premier caractère d'un mot
function ButLast: string; // sauf le dernier caractère d'un mot
// concatène les deux mots, le second en premier
function PutFirst(const St: string): string;
// concatène les deux mots, le premier d'abord
function PutLast(const St: string): string;
// supprime si nécessaire le " initial d'un mot
function WithoutQuote: string;
// supprime si nécessaire le : initial d'un mot
function WithoutColon: string;
function IsValidIdent: Boolean; // est-ce un identificateur valide ?
// les deux mots sont-ils égaux ?
function IsEqual(const St: string): Boolean;
// le premier mot est-il à placer avant le second par ordre alphabétique ?
function IsLower(const St: string): Boolean;
// renvoie le mot qui vient avant par ordre alphabétique
function Lowest(const St: string): string;
// le premier mot est-il à placer après le second par ordre alphabétique ?
function IsGreater(const St: string): Boolean;
// renvoie le mot qui vient après par ordre alphabétique
function Greatest(const St: string): string;
function IsEmptyWord: Boolean; // le mot est-il vide ?
// le mot est-il compris dans un autre ?
function IsMember(const St: string): Boolean;
function Count: Integer; // longueur du mot
function StrCount: string; // longueur de mot en chaîne
function GetItem(const N: Integer): string; // élément N d'un mot
// remplacement de l'élément N d'un mot
function Replace(const N: Integer; const St: string): string;
// suppression de l'élément N d'un mot
function DelItem(const N: Integer): string;
function Reverse: string; // mot inversé
function Shuffle: string; // mot mélangé
function AtRandom: string; // lettre au hasard
function Uppercase: string; // mot en majuscules
function Lowercase: string; // mot en minuscules
// insertion en position N
function Insert(const N: Integer; const St: string): string;
function Sort: string; // tri des lettres du mot
function IsNumber: Boolean; // est-ce un nombre ?
function IsInt: Boolean; // est-ce un entier ?
function IsBoolean: Boolean; // est-ce un booléen ?
function AsNumber: Double; // renvoie un nombre
function AsInt: Integer; // renvoie un entier
function AsBoolean: Boolean; // renvoie un booléen
function IsNegate: Boolean; // nombre négatif ?
function WithEsc: string; // chaîne formatée
function WithoutEsc: string; // chaîne brute
function IsValid: Boolean; // le mot est-il valide sans traitement ?
function Rotate: string; // rotation des caractères d'un mot
property Item[N: Integer]: string read GetItem; default; // élément N
property Text: string read GetText write SetText; // mot à traiter
property FmtText: string read GetFmtText; // mot à traiter formaté
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

La méthode protégée Compare s'occupe des comparaisons : il faut tenir compte de la comparaison de chaînes ordinaires et de nombres. En effet, 99 vient après 121 par ordre alphabétique, mais avant par ordre de grandeur mathématique !

function TGVWord.Compare(const St: string): Integer;
// *** comparaison de deux mots ***
begin
// essai de conversion en nombre des deux mots
fNum.Text := Text;
fNum2.Text := St;
// ce sont des nombres ?
if fNum.IsValid and fNum2.IsValid then
// si oui, on les compare
Result := CompareValue(fNum2.AsDouble, fNum.AsDouble)
else
// on compare les  mots ordinaires
Result := UTF8CompareText(St, Text); // comparaison UTF8
end;

II-C-4-d - Test de l'unité GVWords

Le meilleur moyen de s'approprier les méthodes de la classe TGVWord est de les voir fonctionner dans un programme.

Les versions proposées sont au nombre de deux :

- une version Lazarus Win32 ;

- une version Lazarus Linux.

On trouvera les fichiers sources et les exécutables dans le sous-dossier « 01 - testgvwords » du dossier « tests ».

Si Delphi offre de meilleures capacités à traiter les caractères Unicode et s'il facilite une présentation plus moderne grâce aux styles, Lazarus se montre à la hauteur dans sa portabilité sur différents OS : il n'a pas été nécessaire d'apporter la moindre modification aux fichiers pour faire fonctionner le programme aussi bien sur Windows que sur Linux.

Le programme lui-même est trivial puisqu'il se contente d'affecter le résultat d'une méthode de TGVWord à un TLabel si aucune erreur n'a été déclenchée. C'est exactement ce qui était recherché : l'unité remplit parfaitement son rôle en limitant au possible les complications pour l'utilisateur final. On notera en particulier que les erreurs sont affichées correctement sans que les méthodes d'affichage aient à s'occuper de leur traitement.

Il est intéressant de modifier les paramètres des éditeurs pour voir le comportement de l'unité. Par exemple, on pourra entrer des mots tels que "Essai, cou[cou… On peut aussi entrer une phrase complète et constater qu'elle sera normalisée pour être comprise comme un mot ! Dans tout test, il est conseillé de provoquer des erreurs afin de voir comment se comporte l'unité.

Dans ce petit programme d'exemple, une erreur inhibe toutes les fonctions. Il faut cliquer sur le bouton « Effacer » pour réinitialiser l'ensemble.

Une fonction souvent méconnue est utilisée largement dans ce programme : il s'agit de IfThen. La version traitant les chaînes de caractères est définie dans l'unité strutils. Elle prend deux ou trois paramètres en entrée : le premier est une valeur booléenne, les deux suivants des chaînes de caractères. Si le troisième est omis, il est par défaut défini à la chaîne vide. Le principe de fonctionnement est semblable à celui d'un test avec if…then…else : si la valeur booléenne est vraie, le second paramètre est renvoyé, sinon c'est le troisième.

Par exemple :

procedure TGVMainForm.btnIsValidIdentClick(Sender: TObject);
// test de ISVALIDIDENT
var
LS: string;
begin
fWord.Text := LabEdtFirst.Text;
LS := IfThen(fWord.IsValidIdent, P_True, P_False);
if fWord.Error.OK then
lblResult.Caption := LS;
end;

Le test de la fonction IsValidIdent est effectué sur le composant TLabelEdit : si le résultat est correct, c'est la constante P_True qui est affectée au Caption du composant TLabel, sinon c'est P_False.

Un conseil ("hint") indique que l'unité strutils n'est pas utilisée alors que la compilation échouera si elle est omise ! En effet, elle est indispensable pour le traitement de IfTthen avec des chaînes. Il semblerait que Lazarus soit trompé par l'autre fonction IfThen présente dans l'unité math utilisée par ailleurs.

II-D - Les listes

II-D-1 - Définitions

Une liste simple est composée de mots séparés par des espaces(7). Elle est délimitée par des crochets : [ puis ].

Une liste complexe comprend des mots et des listes. Les imbrications ne sont pas limitées, pourvu que l'ordre d'ouverture et de fermeture des listes imbriquées soit correct.

Il existe une liste particulière qui ne comprend ni mot ni liste. On la nomme la liste vide et elle est représentée par des crochets [] accolés.

II-D-2 - Exemples de listes

II-D-2-a - Listes simples

Voici quelques listes simples :

- [un deux trois quatre cinq]

- [1 2 3 4 5]

- [GVLOGO est un langage facile à apprendre.]

II-D-2-b - Listes imbriquées

Voici des listes complexes :

- [élément1 [liste imbriquée 1] élément3]

- [un [deux imbriquée [encore deux mais imbriquée]] trois]

- [[]]

- [un [deux [deux bis [deux ter]]] trois]

- [un (12*45+789) trois]

La dernière liste contient une expression. Une expression est une suite de caractères à interpréter comme un calcul à effectuer(8).

II-D-2-c - Listes fautives

Une liste est fautive si elle ne contient pas des crochets correctement agencés et en bon nombre.

Voici des listes incorrectes :

- [oups ! [où est la fermeture ?]

- [fermée trop tôt][ceci est une autre liste]

- [le caractère d'échappement masque la fin de liste !$]

- [Une parenthèse se promène seule (]

Pour contourner le problème des crochets ou des parenthèses mal appariés, il suffit d'utiliser le caractère d'échappement, comme avec les mots :

- [le caractère d'échappement ne masque plus la fin de liste !$]]

- [Une parenthèse se promène seule sans problème $(]

Le caractère d'échappement disparaîtra à l'écriture grâce aux primitives ECRIS et TAPE. ECRIST le fera apparaître si nécessaire.

II-D-3 - Opérations sur les listes

Les opérations possibles sur les listes sont en majorité celles déjà vues à propos des mots. Elles sont mentionnées ici avec des exemples propres aux listes.

La primitive ECRIS retire les crochets lors de l'envoi d'une liste à l'écran, tout comme elle fait disparaître le caractère d'échappement. Ce comportement explique pourquoi les exemples qui utilisent cette primitive donnent des résultats sans crochets. Si l'on veut conserver les crochets, il faut utiliser la primitive ECRIST.

II-D-3-a - Fabriquer des listes

METS.PREMIER (raccourci : MP) : attend deux listes en entrée ou un mot et une liste - renvoie une liste - la liste rendue est composée du premier paramètre placé avant le second.

Exemples :

- ECRIS METS.PREMIER [Coucou] [Tortue !] → [Coucou] Tortue !

- ECRIS METS.PREMIER "Coucou [Tortue !] → Coucou Tortue !

METS.DERNIER (raccourci : MD) : attend deux listes en entrée ou un mot suivi d'une liste - renvoie une liste - la liste rendue est composée du premier paramètre placé après le second.

Exemples :

- ECRIS METS.DERNIER [Coucou] [Tortue !] → Tortue ! [Coucou]

- ECRIS METS.DERNIER "Coucou [Tortue !] → Tortue ! Coucou

PHRASE (raccourci : PH) : attend deux listes ou deux mots ou une combinaison des deux en entrée - renvoie une liste - la liste rendue est composée des deux éléments séparés par un espace sans les crochets et/ou sans les guillemets anglais.

Exemples :

- ECRIS PHRASE "je "viens → je viens

- ECRIS PHRASE [je] [pars] → je pars

- ECRIS PHRASE "je [reviens] → je reviens

- ECRIS PHRASE [je] "repars → je repars

II-D-3-b - Modifier des listes

INSERE : attend un entier, un mot ou une liste, puis une liste en entrée - renvoie une liste - la liste rendue est composée du deuxième paramètre inséré à la position précisée par l'entier dans la liste finale.

Exemples :

- ECRIS INSERE 2 "très [c'est bien] → c'est très bien

- ECRIS INSERE 1 [zéro] [[un][deux][trois]] → [zéro][un][deux][trois]

INVERSE : attend une liste en entrée - renvoie une liste - la liste rendue est celle d'entrée dont les éléments ont été inversés.

Exemple :

- ECRIS INVERSE [un deux trois] → trois deux un

MAJUSCULES : attend une liste en entrée - renvoie une liste - la liste rendue est en majuscules.

Exemple :

- ECRIS MAJUCULES [le soleil se lève] → LE SOLEIL SE LÈVE

MELANGE : attend une liste en entrée - renvoie une liste - la liste rendue a les mêmes éléments que celle d'origine, mais dans un ordre aléatoire.

Exemple :

- ECRIS MELANGE [un deux [troisa troisb]] → deux [troisa troisb] un

MINUSCULES : attend une liste en entrée - renvoie une liste - la liste rendue est en minuscules.

Exemple :

- ECRIS MINUSCULES [LES ENFANTS SE LÈVENT] → Les enfants se lèvent

REMPLACE : attend un entier, une liste ou un mot, puis une liste en entrée - renvoie une liste - la liste rendue est composée de tous les éléments de la liste finale, sauf celui visé par l'entier qui a été remplacé par le deuxième paramètre.

Exemples :

- ECRIS REMPLACE 4 "couchent [Les enfants se lèvent] → Les enfants se couchent

- ECRIS REMPLACE 2 [deuxième] [[premier][seconde][troisième]] → [premier][deuxième][troisième]

TRIE : attend une liste en entrée - renvoie une liste - les éléments de la liste sont triés par ordre alphabétique(9).

Exemple :

- ECRIS TRIE [zèbre tartine accent dommage] → accent dommage tartine zèbre

ROTATION : attend une liste en entrée - renvoie une liste - la primitive renvoie la liste dont le premier élément est placé à la fin.

Exemple :

- ECRIS ROTATION [un deux trois quatre] → deux trois quatre un

II-D-3-c - Extraire de listes

ELEMENT : attend un entier suivi d'une liste en entrée - renvoie une liste ou un mot - l'objet rendu est l'élément de la liste en entrée indiqué par l'entier.

Exemple :

- ECRIS ELEMENT 2 [un deux [trois]] → deux

DERNIER (raccourci : DER) : attend une liste en entrée - renvoie un mot ou une liste - l'objet renvoyé est composé du dernier élément de la liste en entrée.

Exemple :

- ECRIS DERNIER [un deux] → deux

HASARD : attend une liste en entrée - renvoie un mot ou une liste - l'objet renvoyé est composé d'un élément de la liste en entrée, tiré au hasard.

Exemple :

- ECRIS HASARD [un [deux] trois quatre] → trois

PREMIER (raccourci : PREM) : attend une liste en entrée - renvoie un mot ou une liste - l'objet renvoyé est composé du premier élément de la liste en entrée.

Exemple :

- ECRIS PREMIER [beau laid] → beau

PREMS : attend une liste en entrée - renvoie une liste - l'objet renvoyé est composé de chaque premier élément des éléments de la liste en entrée.

Exemples :

- ECRIS PREMS [beau [laid]] → b laid

- ECRIS PREMS [[moi beau] [toi laid]] → [moi] [toi]

SAUF.DERNIER (raccourci : SD) : attend une liste en entrée - renvoie un mot ou une liste - l'objet renvoyé est composé de la liste en entrée sans son dernier élément.

Exemple :

- ECRIS SAUF.DERNIER [pas cette liste stupide] → pas cette liste

SAUF.PREMIER (raccourci : SP) : attend une liste en entrée - renvoie un mot ou une liste - l'objet renvoyé est composé de la liste en entrée sans son premier élément.

Exemple :

- ECRIS SAUF.PREMIER [pas cette liste stupide] → cette liste stupide

SAUF.PREMS : attend une liste en entrée - renvoie une liste - l'objet renvoyé est composé de la liste en entrée sans chaque premier élément de ses éléments.

Exemples :

- ECRIS SAUF.PREMS [pas cette liste stupide] → as ette iste tupide

- ECRIS SAUF.PREMS [[un chat] [une chouette]] → [chat] [chouette]

II-D-3-d - Tester des listes

PLG? : attend deux listes en entrée - renvoie un booléen - la primitive renvoie "VRAI si la première liste vient strictement après la seconde selon l'ordre alphabétique, "FAUX sinon.

Exemple :

- ECRIS PLG? [un petit chat] [un chat] → -1

PLP? : attend deux listes en entrée - renvoie un booléen - la primitive renvoie "VRAI si la première liste vient strictement avant la seconde selon l'ordre alphabétique, "FAUX sinon.

Exemple :

- ECRIS PLP? [le renard] [un renard] → -1

COMPTE : attend une liste en entrée - renvoie un entier - la primitive renvoie le nombre d'éléments de la liste en entrée.

Exemple :

- ECRIS COMPTE [un [deux encore deux] trois] → 3

EGAL? : attend deux listes en entrée - renvoie un booléen - la primitive renvoie "VRAI si la première liste est identique à la seconde, "FAUX sinon.

Exemple :

- ECRIS EGAL? [c'est la même] METSPREMIER "c'est [la même] → -1

MEMBRE? : attend une liste ou un mot, puis une liste en entrée - renvoie un booléen - la primitive renvoie "VRAI si le premier objet est compris dans le second, "FAUX sinon.

Exemples :

- ECRIS MEMBRE? [oui] [un deux [oui]] → -1

- ECRIS MEMBRE? "oui [un deux [oui]] → 0

LISTE? : attend un objet en entrée - renvoie un booléen - la primitive renvoie "VRAI si l'objet est une liste, "FAUX sinon.

Exemples :

- ECRIS LISTE? [coucou] → -1

- ECRIS LISTE? "coucou → 0

PRECEDENT : attend deux listes en entrée - renvoie une liste - la primitive renvoie la liste qui vient la première selon l'ordre alphabétique.

Exemple :

- ECRIS PRECEDENT [je suis premier] [je suis second] → je suis premier

SUIVANT : attend deux listes en entrée - renvoie une liste - la primitive renvoie la liste qui vient la dernière selon l'ordre alphabétique.

Exemple :

- ECRIS SUIVANT [je suis premier] [je suis second] → je suis second

VIDE? : attend une liste en entrée - renvoie un booléen - la primitive renvoie "VRAI si la liste en entrée est la liste vide, "FAUX sinon.

Exemples :

- ECRIS VIDE? [] → -1

- ECRIS VIDE? SAUF.PREMIER [un] → -1

II-D-4 - Implémentation des listes

L'implémentation des listes passe par un complément de l'unité GVConsts qui contient les constantes centralisées du projet et une unité baptisée GVLists qui abrite les classes nécessaires à la gestion proprement dite des listes. Cette unité utilise par ailleurs l'unité GVWords précédemment étudiée.

II-D-4-a - Constantes

Les constantes ont simplement été complétées avec les chaînes nécessaires au signalement des erreurs et un type énuméré baptisé TGVError qui reprend les erreurs sous forme codée.

II-D-4-b - La classe TGVListUtils

La classe TGVListUtils concentre les méthodes utiles au traitement des listes. Il aurait été possible de les implémenter en tant que procédures et fonctions indépendantes, mais l'utilisation d'une classe et de son interface permet une meilleure maîtrise des outils : la classe définit précisément ce pour quoi elle est faite.

En voici l'interface :

// *** classe des utilitaires pour les listes ***
TGVListUtils = class
strict private
fError: TGVErrors; // traitement des erreurs
fWord: TGVWord; // mot de travail
fPos: Integer; // position de travail
fSt: TGVString; // chaîne de travail
public
constructor Create; // constructeur
destructor Destroy; override; // destructeur
procedure Clear; // nettoyage
// conversion d'une liste en mot
function ListToWord(const St: string): string;
// conversion d'un mot en liste
function WordToList(const St: string): string;
// conversion d'une liste en chaîne
function ListToStr(const St: string): string; overload;
function ListToStr(const St: string; out ASt: string): Boolean; overload;
// conversion d'une chaîne en liste
function StrToList(const St: string): string;
// retourne la liste vide
function EmptyList: string;
// vérifie la validité d'une liste
function IsValid(const St: string): Boolean;
// teste la validité d'une valeur (mot ou liste)
function IsValidValue(const St: string): Boolean;
// liste simple ?
function IsSimpleList(const St: string): Boolean;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

En dehors de la fonction EmptyList qui se contente de renvoyer la liste vide, il s'agit essentiellement de méthodes de conversion et de validation.

Ainsi ListToStr et StrToList convertissent une liste en chaîne et réciproquement. ListToWord et WordToList prennent en compte le caractère d'échappement. Pour ces quatre méthodes, un contrôle de la validité des listes est effectué.

Le contrôle de la validité d'une liste s'effectue grâce à IsValid qui est de loin la plus complexe. Elle analyse une chaîne et contrôle caractère par caractère que les règles relatives aux listes sont respectées : autant de crochets ouvrants que de crochets fermants, dans le bon ordre ; autant de parenthèses ouvrantes que de parenthèses fermantes, dans le bon ordre ; prise en compte du caractère d'échappement ; interdiction des sous-listes à l'intérieur d'une expression parenthésée.

Les autres méthodes de contrôle sont plus simples. IsSimpleValue se contente de vérifier la présence des crochets pour une liste. IsValidValue exploite l'unité GVWords pour tester un mot et la méthode précédente pour une liste. TestValue est une procédure qui déclenche une exception si la chaîne en entrée n'est ni un mot ni une liste au sens de GVLOGO.

II-D-4-c - La classe TGVList

La classe TGVList implémente la gestion des listes pour GVLOGO. Elle hérite de TStringList qui gère les listes de chaînes. En fait, une liste en LOGO est une chaîne particulière de caractères : elle nécessite d'être entourée de crochets et, si elle comprend d'autres listes, de comprendre à l'intérieur autant de crochets ouvrants que de crochets fermants (dans le bon ordre évidemment !). Cette dernière remarque s'applique aussi pour les éventuelles expressions imbriquées dans la liste. Pour parvenir à cet effet, il faut surcharger les méthodes d'entrée : Add, LoadFromStream, Assign, Insert et Put. Chaque élément de la liste (mot ou sous-liste) est stocké dans une chaîne de la TStringList.

Toutes les nouvelles méthodes renvoient une chaîne ou une valeur sans modifier la liste d'entrée. En cas d'erreur lors de la construction de la liste, la liste interne est la chaîne vide et la propriété LastErrorPos renvoie l'indice de l'erreur détectée.

En voici l'interface(10) :

TGVList = class(TStringList)
strict private
fRawStr: string; // chaîne brute en entrée
fError: TGVErrors; // traitement des erreurs
fIsValid: Boolean; // validité de la liste
fPos: Integer; // position de travail
fLoading: Boolean; // chargement en cours ?
fNumLastItem: Integer; // dernier élément trouvé
fWord: TGVWord; // mot de travail
fUtil: TGVListUtils; // utilitaire pour liste
protected
procedure Put(Index: Integer; const St: string); override; // assignation
function Get(Index: Integer): string; override; // élément choisi
function GetTextStr: string; override; // récupération du texte
procedure SetTextStr(const AValue: string); override; // validation du texte
public
constructor Create; overload; // création
destructor Destroy; override; // destruction
procedure Clear; override; // nettoyage
function Add(const St: string): Integer; override; // ajout
procedure LoadFromStream(Stream: TStream); overload; override; // chargement
procedure Assign(Source: TPersistent); override; // assignation
procedure Insert(Index: Integer; const St: string); override; // insertion
// nouvelles méthodes (*** ne modifient pas la liste interne ***)
// renvoie la liste sous forme de chaîne
function ToStr: string;
// renvoie la liste sous forme de chaîne sans crochets
function ToWBStr: string;
// la liste est-elle la liste vide ?
function IsEmptyList: Boolean;
// renvoie le premier élément de la liste
function First: string;
// renvoie le dernier élément de la liste
function Last: string;
// sauf le premier de la liste
function ButFirst: string;
// sauf le dernier de la liste
function ButLast: string;
// supprime l'élément N
function DeleteItem(N: Integer): string;
// insertion d'un élément en position N
function InsertAItem(N: Integer; const St: string): string;
// remplacement de l'élément N
function ReplaceItem(N: Integer; const St: string): string;
// met en premier
function PutFirst(const St: string): string;
// met en dernier
function PutLast(const St: string): string;
// phrase à droite
function SentenceRight(const St: string): string;
// phrase à gauche
function SentenceLeft(const St: string): string;
// tri des éléments
function SortItems: string;
// inversion des éléments
function ReverseItems: string;
// mélange des éléments
function ShuffleItems: string;
// membre présent ?
function IsItem(const St: string): Boolean;
// ajout d'une paire
function TwoAdd(const St1, St2: string): string;
// suppression d'une paire
function TwoDelete(const N: Integer): string;
// liste en majuscules
function UpperCase: string;
// liste en minuscules
function LowerCase: string;
// rotation de la liste
function Rotate: string;
// élément de la liste choisi au hasard
function AtRandom: string;
// premier élément de chaque élément d'une liste
function Firsts: string;
// tout sauf le premier élément de chaque élément d'une liste
function ButFirsts: string;
// dernier élément traité
property LastItem: Integer read fNumLastItem default -1;
// chaîne brute en entrée
property RawStr: string read fRawStr; // chaîne brute en entrée
// notification d'une erreur
property Error: TGVErrors read fError write fError;
// validité de la liste
property IsValid: Boolean read fIsValid default False;
// acquisition du texte
property Text: string read GetTextStr write SetTextStr;
end;

La méthode la plus complexe est certainement Add. Elle prend en charge l'ensemble du contrôle de la validité de la liste. Cette méthode comprend une partie générale qui teste les mots et les listes avant de renvoyer une éventuelle position d'erreur ou de stocker les chaînes trouvées, et une procédure imbriquée qui examine le contenu de la chaîne. Cette dernière lit les caractères un par un et répartit le travail suivant ce qu'elle rencontre : blanc, crochet ouvrant, parenthèse ouvrante, échappement, crochet et parenthèse fermants mais orphelins, autre caractère.

Elle est en cela similaire à la méthode IsValid de TGVListUtils, sinon qu'elle stocke les éléments en vue de leur exploitation. Du point de vue de l'utilisateur, l'entrée d'une nouvelle liste se fait grâce à la propriété héritée Text de l'unité : il suffit d'affecter une chaîne à cette propriété pour qu'elle soit analysée et stockée.

La propriété LastItem n'est affectée que par l'utilisation de la méthode IsItem. Si cette dernière renvoie True, LastItem contiendra le numéro de l'élément de la liste qui correspond à celui recherché.

On notera que les éléments des listes en Pascal sont numérotés avec une base de 0 alors qu'en GVLOGO la base est de 1, ce qui paraîtra plus naturel à l'utilisateur.

II-D-4-d - Test de l'unité TGVLists

Comme lors du test de TGVWords, les versions proposées sont au nombre de deux :

- une version Win32 ;

- une version Linux.

Le programme lui-même ne pose pas de problèmes particuliers. Il ne s'agit en effet que de mettre à jour la liste de travail et d'appliquer la méthode voulue.

Il est encore une fois conseillé de modifier les différents paramètres pour observer les réactions de l'unité.

II-E - Les listes de propriétés

II-E-1 - Définitions

Les listes de propriétés sont des listes particulières associant chacune à un nom des couples propriété-valeur.

Par exemple, on peut imaginer répertorier des chiens dont les caractéristiques seront stockées ainsi : Zoé aura pour propriétés race, sexe, couleur, taille et âge dont les valeurs respectives seront caniche, femelle, blanc, très petit, 5. Médor aura les propriétés race, sexe, âge, caractère dont les valeurs seront teckel, mâle, 3, tranquille. On voit que l‘agencement des propriétés est libre, contrairement à ce qui se passerait pour un tableau.

GVLOGO fait un usage interne intensif des listes de propriétés. En effet, aussi bien les variables, les procédures, les paquets que les lignes des éditeurs sont des listes de propriétés.

Les noms donnés aux propriétés tout comme le nom des listes sont indifférents aux majuscules et minuscules. Par conséquent, les propriétés MAPROP, MaProp ou encore maprop sont équivalentes. En revanche, ce nom doit être un identificateur correct.

II-E-2 - Exemples de listes de propriétés

- Liste associée à "caniche" : [couleur blanc taille [tout petit]]

- Liste associée à "labrador" : [couleur blanc vitesse [rapide] ans 8]

Les propriétés d'une liste de propriétés sont organisées en deux éléments consécutifs : le premier définit le nom de la propriété tandis que le second indique la valeur qui lui est associée. Le premier est toujours un mot unique pour la liste considérée alors que le second peut être un mot ou une liste quelconques.

II-E-3 - Opérations sur les listes de propriétés

DPROP : attend deux mots en entrée suivis d'un mot ou une liste - ne renvoie rien - la primitive crée ou met à jour la propriété de la liste de propriétés fournie en paramètre. Le premier mot est la liste de propriétés et le second la propriété. Le dernier paramètre est soit un mot soit une liste qui définit la valeur attribuée à la propriété.

Exemple :

- DPROP "MALISTE "MAPROP [une valeur] → -

RPROP : attend deux mots en entrée - renvoie un mot ou une liste - la primitive renvoie la valeur de la propriété (second paramètre) de la liste visée (premier paramètre).

Exemple :

- ECRIS RPROP "MALISTE "MAPROP → une valeur

ANPROP : attend deux mots en entrée - ne renvoie rien - la propriété (second paramètre) de la liste spécifiée (premier paramètre) est supprimée.

Exemple :

- ANPROP "MALISTE "MAPROP → -

PROPS : attend un mot en entrée - renvoie une liste - la primitive renvoie la liste des propriétés de la liste en entrée.

Exemple :

- ECRIS PROPS "MALISTE → maprop

ANNULE : attend un mot en entrée - ne renvoie rien - la liste spécifiée par le mot est détruite avec toutes ses propriétés.

Exemple :

- ANNULE "MALISTE → -

COMPTE.PROPS : attend un mot en entrée - renvoie un entier - la primitive renvoie le nombre de propriétés associées à la liste en entrée.

Exemple :

- COMPTEPROPS "MALISTE → 1

PROP? : attend deux mots en entrée - renvoie un booléen - la primitive renvoie "VRAI si le second mot en entrée est une propriété du premier, "FAUX sinon

Exemple :

- ECRIS PROP? "MALISTE "MAPROP → -1

LISTE.PROP? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est une liste de propriétés, "FAUX sinon.

Exemple :

- ECRIS LISTEPROP? "MALISTE → -1

PROCEDURE? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est une procédure, "FAUX sinon.

Exemple :

- ECRIS PROCEDURE? "MALISTE → 0

PRIMITIVE? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est une primitive, "FAUX sinon.

Exemple :

- ECRIS PRIMITIVE? "ECRIS → 0

On notera que la primitive (ou la procédure) est précédée des guillemets anglais. Si elle ne l'était pas, GVLOGO chercherait à l'exécuter, ce qui n'est pas l'effet voulu.

NOM? : attend un mot en entrée - renvoie un booléen - la primitive renvoie "VRAI si le mot en entrée est une variable, "FAUX sinon.

Exemple :

- ECRIS NOM? "MALISTE → 0

II-E-4 - Implémentation des listes de propriétés

L'unité GVPropLists implémente essentiellement deux classes : TGVPropListEnumerator pour l'énumération et TGVPropList pour les listes elles-mêmes.

II-E-4-a - Constantes

Comme pour les unités précédentes, l'unité GVConsts a été complétée afin de prendre en compte les nouveaux besoins. Les ajouts ont été effectués au niveau des constantes elles-mêmes.

On remarquera l'extension pour les fichiers et surtout l'en-tête qui permettra de vérifier qu'un fichier est conforme au format attendu : il précèdera les données proprement dites.

Les chaînes de ressources ont aussi été complétées, de même pour l'énumération des erreurs possibles.

II-E-4-b - La classe TGVPROPLISTENUMERATOR

Comme son nom l'indique, l'énumération est un moyen de dresser la liste des éléments contenus dans un objet. Aussi bien Lazarus que Delphi en font un usage étendu à travers les objets tels que les listes.

La technique d'implémentation, finalement assez simple, obéit à un schéma type : on doit fournir l'élément en cours (pointé par une propriété en lecture seule) et être capable d'indiquer si l'on peut se déplacer vers l'élément suivant.

Cela donne dans le cas des listes de propriétés l'interface suivante :

type
// *** classe pour énumération ***
TGVPropListEnumerator = class(TObject)
private
fLst: TStringList;
fIndex: Integer;
protected
function GetCurrent: string; virtual; // élément en cours
public
constructor Create(const Value: TStrings); // création
destructor Destroy; override; // destruction
function MoveNext: Boolean; // vers le suivant
property Current: string read GetCurrent; // valeur de l'élément courant
end;

Comme les listes de propriétés fonctionnent via une liste interne, il suffit d'en manipuler les éléments grâce à un index.

La gestion se fait au moyen du système intégré dans TStringList des paires Names/Values, mais utilise un caractère séparateur personnalisé : | (11). Ce caractère ne doit par conséquent pas figurer dans la liste elle-même.

Les utilisateurs de Delphi pourront imaginer une unité fondée sur la classe TDictionary.

II-E-4-c - La classe TGVPROPLIST

La classe TGVPropList fonctionne grâce à un champ privé fNames de type TStringList. Les méthodes sont organisées en trois blocs : méthodes générales, celles concernant les listes de propriétés et celles concernant les propriétés.

En voici l'interface :

// *** classe des listes de propriétés ***
TGVPropList = class(TObject)
strict private
fError: TGVErrors; // enregistrement d'une erreur
fNames: TStringList; // listes
fOnchange: TNotifyEvent; // notification de changement
function GetLPByNum(N: Integer): string; // liste par numéro
function GetLPByName(const Name: string): string; // renvoie liste par nom
procedure SetLPByName(const Name, Value: string); // fixe liste par nom
protected
procedure Change; // changement
public
// constructeur de la classe
constructor Create;
// destructeur de la classe
destructor Destroy; override; // destructeur
// énumération
function GetEnumerator: TGVPropListEnumerator;
// nettoie les listes de propriétés
procedure Clear;
// *** listes de propriétés ***
// renvoie la liste des listes de propriétés
function ListP: string;
// la liste existe-t-elle ?
function IsListP(const Name: string): Boolean;
// renvoie le numéro d'une liste de propriétés
function NumListP(const Name: string): Integer;
// crée ou met à jour la liste de propriétés
function UpDateListP(const Name, Prop, Value: string): Boolean;
// renvoie la valeur d'une liste
function ValListP(const Name: string): string;
// destruction d'une liste de propriétés
function RemoveListP(const Name: string): Boolean;
// valeur d'une liste de propriétés par numéro
function ValNumListP(N: Integer; out Name, Value: string): Boolean;
// la propriété N existe-t-elle?
function IsListPByNum(N: Integer): Boolean;
// renvoie le nombre de listes de propriétés
function CountListP: Integer;
// chargement des listes
procedure LoadFromFile(const FileName: string);
// sauvegarde des listes
procedure SaveToFile(const FileName: string);
// *** propriétés
// la propriété existe-t-elle ?
function IsProp(const Name, Prop: string): Boolean;
// renvoie le numéro d'une propriété
function NumProp(const Name, Prop: string): Integer;
// valeur d'une propriété
function ValProp(const Name, Prop: string): string;
// destruction d'une propriété
function RemoveProp(const Name, Prop: string): Boolean;
// renvoie le nombre de propriétés attachées à une liste
function CountProps(const Name: string): Integer;
// valeur d'une propriété par numéro
function ValNumProp(const Name: string; N: Integer; out Prop: string)
: Boolean; overload;
function ValNumProp(const Name: string; N: Integer): string;
// liste des propriétés d'une liste
function ListOfProps(const Name: string): string;
// nom d'une propriété par numéro
function NameOfProp(const Name: string; N: Integer): string; overload;
// changement dans la liste de propriétés
property OnChange: TNotifyEvent read fOnchange write fOnchange;
// liste de propriétés par numéro
property ListPByNum[N: Integer]: string read GetLPByNum; default;
// liste de propriétés par nom
property LPByName[const Name: string]: string read GetLPByName
write SetLPByName;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

On remarquera que la propriété ListPByNum est définie comme default, ce qui signifie qu'elle peut être omise en employant simplement le numéro de la liste entre crochets.

Parmi les difficultés, on relèvera :

- Les listes de propriétés sont numérotées à partir de 1 et non de 0 comme en Pascal ;

- La méthode RemoveProp détruit aussi la liste si elle ne contient plus aucune propriété ;

- La méthode UpdateListP crée et met à jour les listes et les propriétés ;

- Il est fait un usage intensif de la fonction Odd qui vérifie qu'un nombre est impair : en effet, comme les propriétés sont organisées par paires, il est nécessaire de bien différencier le nom de la propriété (élément impair) de sa valeur (élément pair).

II-E-4-d - Test de l'unité TGVPROPLists

Comme d'habitude, le programme de test est décliné en deux versions :

- Lazarus pour Windows 32 ;

- Lazarus pour Linux.

Un fichier de listes préenregistrées permet de tester immédiatement LoadFromFile : « ListPs.GPL ». Il peut être édité par n'importe quel lecteur de textes simples (genre NotePad).

II-F - La tortue graphique

II-F-1 - Présentation

La tortue graphique est sans doute l'objet le plus connu du langage LOGO. Elle a fait son succès, mais l'a aussi très longtemps limité à une utilisation avec des enfants, conduisant les utilisateurs à négliger les listes qui permettent d'aborder des sujets plus ardus.

La tortue graphique est habituellement symbolisée par un triangle sur une surface de dessin. GVLOGO propose par ailleurs une tortue plus réaliste au format « png ». Des ordres permettent de la faire dessiner. Les commandes disponibles sont multiples : l'utilisateur maîtrise son orientation, son trait, sa forme, sa couleur…

L'originalité de ce mode de dessin réside dans sa capacité à partir d'une orientation dans l'espace déterminée par la tortue elle-même : même s'il est possible de dessiner dans un repère orthonormé traditionnel, la plupart des commandes sont dépendantes de l'état en cours de la tortue. Ainsi, tourner de 90° à gauche se fera par rapport à l'orientation actuelle de la tortue : si son cap était de 45° par rapport à la surface de dessin, son nouveau cap sera de 135°.

Par défaut, la tortue est au centre de la surface de dessin qui se mesure en pixels et dépend de la taille du composant TPanel d'accueil : sa position d'origine est donc le centre de ce composant. Ses déplacements se comptent en pixels si les échelles des abscisses et des ordonnées sont fixées à 100 (valeur par défaut). Un déplacement négatif fait reculer la tortue.

Le cap de la tortue indique la direction des déplacements. Il est exprimé en degrés et va de 0 à 360. Le cap est de 90° par défaut et pointe vers le sommet de l'écran afin d'être en conformité avec le cercle trigonométrique. On visualise ce cap grâce à l'orientation du triangle qui symbolise la tortue : l'arrière de la tortue est un trait plus épais ; sa tête est donc le sommet du triangle qui marque l'intersection des deux lignes plus fines.

II-F-2 - Opérations avec la tortue

II-F-2-a - Champ de la tortue

Le champ de la tortue est la surface sur laquelle elle peut évoluer. Le coin inférieur gauche de l'écran est la position d'abscisse 0 et d'ordonnée 0. Par défaut, le point supérieur droit a pour abscisse la largeur du composant d'accueil et pour ordonnée sa hauteur(12).

CLOS : n'attend rien en entrée - ne renvoie rien - le champ de la tortue est limité à l'écran visible. Toute tentative de sortir de l'écran se soldera par un échec.

Exemple :

- CLOS → -

Le champ est défini à FENETRE par défaut.

ENROULE : n'attend rien en entrée - ne renvoie rien - le champ de la tortue s'enroule sur lui-même : quand la tortue atteint un bord, elle réapparaît sur le bord opposé.

Exemple :

- ENROULE → -

FENETRE (raccourci : FEN) : n'attend rien en entrée - ne renvoie rien - le champ de la tortue s'étend au-delà de l'écran. La tortue peut donc disparaître du champ de vision de l'utilisateur.

Exemple :

- FENETRE → -

ETAT.ECRAN : n'attend rien en entrée - renvoie une liste - la liste renvoyée indique l'état actuel de l'écran avec les données suivantes : couleur, hauteur et largeur.

Exemple :

- ECRIS ETAT.ECRAN → 0 700 677

FIXE.ECHELLE : attend une liste de deux entiers en entrée - ne renvoie rien - la primitive détermine les proportions du déplacement de la tortue. Par défaut, la valeur pour les abscisses (premier entier) et pour les ordonnées (second paramètre) est de 100. Une valeur moindre réduira les déplacements dans la proportion indiquée et une valeur supérieure l'augmentera. Par exemple, fixer le premier entier à 200 et le second à 50 fera que la tortue se déplacera deux fois plus vite horizontalement que la normale et deux fois plus lentement verticalement.

Exemple :

- FIXE.ECHELLE [200 50] → -

FIXE.ECHELLEX : attend un entier en entrée - ne renvoie rien - la primitive détermine la proportion du déplacement de la tortue selon l'axe des abscisses.

Exemple :

- FIXE.CHELLEX 200 → -

FIXE.ECHELLEY : attend un entier en entrée - ne renvoie rien - la primitive détermine la proportion du déplacement de la tortue selon l'axe des ordonnées.

Exemple :

- FIXE.ECHELLEY 50 → -

ECHELLE : n'attend rien en entrée - renvoie une liste de deux entiers - la liste renvoyée contient un premier entier qui indique l'échelle des déplacements de la tortue selon l'axe des abscisses et un second entier qui indique l'échelle selon l'axe des ordonnées.

Exemple :

- ECRIS ECHELLE → 100 100

VIDE.ECRAN (raccourci : VE) : n'attend rien en entrée - ne renvoie rien - l'espace de la tortue est nettoyé et la tortue retrouve sa place et son cap d'origine, ainsi que toutes ses valeurs par défaut.

Exemple :

- VIDE.ECRAN → -

ORIGINE : n'attend rien en entrée - ne renvoie rien - la tortue retrouve sa position d'origine en [300 300] et son cap initial de 90. L'état du crayon n'est pas modifié. Si le crayon est baissé, le déplacement de la tortue laisse une trace.

Exemple :

- ORIGINE → -

NETTOIE : n'attend rien en entrée - ne renvoie rien - l'espace de la tortue est nettoyé, sans changer la position de la tortue ni son cap, pas plus que les valeurs du crayon.

Exemple :

- NETTOIE → -

FIXE.COULEUR.FOND (raccourci : FCF) : attend un entier en entrée - ne renvoie rien - la primitive détermine la couleur de fond de l'écran de la tortue.

Exemple :

- FIXE.COULEUR.FOND 19 → -

Tableau 1 - Couleurs standards de la tortue

Noir

0

Bleu ciel

1

Bleu

2

Crème

3

Gris foncé

4

Fuchsia

5

Gris

6

Vert

7

Vert citron

8

Gris clair

9

Marron

10

Gris moyen

11

Vert menthe

12

Bleu marine

13

Vert olive

14

Violet

15

Rouge

16

Argent

17

Bleu ciel

18

Sarcelle

19

Blanc

20

Jaune

21

COULEUR.FOND (raccourci : CF) : n'attend rien en entrée - renvoie un entier - l'entier renvoyé correspond à la couleur du fond de l'écran de la tortue.

Exemple :

- ECRIS COULEUR.FOND → 19

DISTANCE : attend deux entiers en entrée - renvoie un entier - l'entier renvoyé correspond à la distance de la tortue au point de coordonnées fournies en entrée (abscisse puis ordonnée).

Exemple :

- ECRIS DISTANCE 50 50 → 190

II-F-2-b - Déplacements de la tortue

AVANCE (raccourci : AV) : attend un entier en entrée - ne renvoie rien - la tortue avance du nombre de pixels indiqué par le paramètre en entrée, sauf si les échelles ont été changées. Elle laisse une trace sur la zone de dessin à moins que son crayon ne soit levé ou qu'elle soit en mode gomme.

Exemple :

- AVANCE 45 → -

RECULE (raccourci : RE) : attend un entier en entrée - ne renvoie rien - la tortue recule du nombre de pixels indiqué par le paramètre en entrée, sauf si les échelles ont été changées. Elle laisse une trace sur la zone de dessin à moins que son crayon ne soit levé ou qu'elle soit en mode gomme.

Exemple :

- RECULE 50 → -

GAUCHE (raccourci : TG) : attend un réel en entrée - ne renvoie rien - la tortue tourne à gauche du nombre de degrés indiqué en entrée.

Exemple :

- GAUCHE 90 → -

Pour rappel : la gauche indiquée est celle par rapport à la tortue et non la gauche de l'écran ! Cette remarque vaut évidemment pour la primitive DROITE.

DROITE (raccourci : TD) : attend un réel en entrée - ne renvoie rien - la tortue tourne à droite du nombre de degrés indiqué en entrée.

Exemple :

- DROITE 90 → -

FIXE.POS (raccourci : FPOS) : attend une liste de deux entiers en entrée - ne renvoie rien - la tortue est envoyée à la position indiquée par la liste : le premier élément fixe l'abscisse tandis que le second détermine l'ordonnée.

Exemple :

- FIXE.POS [200 100] → -

FIXE.XY (raccourci : FXY) : attend deux entiers en entrée - ne renvoie rien - la tortue est envoyée à la position indiquée par les paramètres : le premier élément fixe l'abscisse tandis que le second détermine l'ordonnée.

Exemple :

- FIXE.XY 200 100 → -

FIXE.X (raccourci : FX) : attend un entier en entrée - ne renvoie rien - l'abscisse de la tortue est fixée à l'entier fourni en entrée.

Exemple :

- FIXE.X 200 → -

FIXE.Y (raccourci : FY) : attend un entier en entrée - ne renvoie rien - l'ordonnée de la tortue est fixée à l'entier fourni en entrée.

Exemple :

- FIXE.Y 100 → -

POS : n'attend rien en entrée - renvoie une liste de deux entiers - la liste renvoyée contient un premier entier qui indique l'abscisse de la tortue et un second entier qui indique son ordonnée.

Exemple :

- ECRIS POS → 200 100

XCOOR : n'attend rien en entrée - renvoie un entier - l'entier renvoyé indique l'abscisse de la tortue.

Exemple :

- ECRIS XCOOR → 200

YCOOR : n'attend rien en entrée - renvoie un entier - l'entier renvoyé indique l'ordonnée de la tortue.

Exemple :

- ECRIS YCOOR → 100

FIXE.VITESSE : attend un entier en entrée - ne renvoie rien - la vitesse de la tortue est fixée à l'entier fourni en entrée.

Exemple :

- FIXEVITESSE 100 → -

La vitesse est relative aux performances de l'ordinateur sur lequel est installé GVLOGO. Elle est automatiquement limitée à 100.

VITESSE : n'attend rien en entrée - renvoie un entier - l'entier renvoyé indique la vitesse de dessin de la tortue.

Exemple :

- ECRIS VITESSE → 100

II-F-2-c - Cap de la tortue

FIXE.CAP (raccourci : FCAP) : attend un réel en entrée - ne renvoie rien - le cap de la tortue est fixé au nombre fourni en entrée. Le cap est compris entre 0 et 360.

Exemple :

- FIXECAP 90 → -

CAP : n'attend rien en entrée - renvoie un entier - l'entier renvoyé indique le cap de la tortue.

Exemple :

- ECRIS CAP → 90

VERS : attend une liste de deux entiers en entrée - renvoie un entier - L'entier renvoyé indique le cap que devrait avoir la tortue pour pointer vers le point dont les coordonnées sont fournies en entrée.

Exemple :

- VERS [0 0] → 214

Pour rappel : le cap 90 est celui qui pointe vers le haut de l'écran. Le cap 0 pointe vers sa droite. En fait, le cap augmente en tournant dans le sens inverse des aiguilles d'une montre.

II-F-2-d - État de la tortue

MONTRE.TORTUE (raccourci : MT) : n'attend rien en entrée - ne renvoie rien - la tortue devient visible.

Exemple :

- MONTRE.TORTUE → -

CACHE.TORTUE (raccourci : CT) : n'attend rien en entrée - ne renvoie rien - la tortue devient invisible.

Exemple :

- CACHE.TORTUE → -

La tortue dessine plus rapidement lorsqu'elle est invisible.

VISIBLE? : n'attend rien en entrée - renvoie un booléen - la primitive renvoie "VRAI si la tortue est visible, "FAUX sinon.

Exemple :

- ECRIS VISIBLE? → -1

ETAT.TORTUE : n'attend rien en entrée - renvoie une liste - la liste renvoyée fournit les données relatives à la tortue : son abscisse, son ordonnée, son cap, sa taille, sa vitesse, sa visibilité et si elle est figurée par un triangle.

Exemple :

- ECRIS ETAT.TORTUE → 300 300 98 8 100 -1 0

FIXE.ETAT.TORTUE : attend une liste en entrée - ne renvoie rien - l'état de la tortue est fixé selon le contenu de la liste : abscisse, ordonnée, orientation, taille, vitesse, visibilité et type de tortue. La tortue dessine durant le changement si le crayon est baissé.

Exemple :

- FIXE.ETAT.TORTUE [100 100 0 8 100 -1 -1] → -

TORTUE.NORMALE : n'attend rien en entrée - ne renvoie rien - quand elle est visible, la tortue est symbolisée par un triangle. C'est le mode par défaut.

Exemple :

- TORTUE.NORMALE → -

TORTUE.VERTE : n'attend rien en entrée - ne renvoie rien - quand elle est visible, la tortue est dessinée à partir d'images au format PNG.

Exemple :

- TORTUE.VERTE → -

L'image est plus sympathique, mais le dessin est un peu plus lent et la précision pour les angles moindre (5°).

FIXE.TAILLE : attend un entier en entrée - ne renvoie rien - la taille de la tortue est fixée au nombre fourni en entrée.

Exemple :

- FIXE.TAILLE 10 → -

La taille ne peut excéder 20 et ne concerne que le type par défaut, à savoir le triangle. La valeur par défaut est de 8.

TAILLE : n'attend rien en entrée - renvoie un entier - l'entier renvoyé indique la taille actuelle de la tortue.

Exemple :

- ECRIS TAILLE → 8

Si la tortue est la tortue png, la taille renvoyée n'aura pas de sens puisqu'elle ne varie pas !

II-F-2-e - Le crayon

BAISSE.CRAYON (raccourci : BC) : n'attend rien en entrée - ne renvoie rien - le crayon est baissé et donc dessine avec les attributs qui lui ont été affectés. C'est la position par défaut du crayon.

Exemple :

- BAISSE.CRAYON → -

LEVE.CRAYON (raccourci : LC) : n'attend rien en entrée - ne renvoie rien - le crayon est levé et donc ne dessine pas lorsque la tortue est déplacée.

Exemple :

- LEVE.CRAYON → -

BAISSE? : n'attend rien en entrée - renvoie un booléen - la primitive renvoie "VRAI si le crayon est baissé, "FAUX sinon.

Exemple :

- ECRIS BAISSE? → -1

FIXE.COULEUR.CRAYON (raccourci : FCC) : attend un entier en entrée - ne renvoie rien - la primitive détermine la couleur d'écriture du crayon de la tortue(13).

Exemple :

- FIXE.COULEUR.CRAYON 5 → -

COULEUR.CRAYON (raccourci : CC) : n'attend rien en entrée - renvoie un entier - l'entier renvoyé correspond à la couleur du crayon de la tortue.

Exemple :

- ECRIS COULEUR.CRAYON → 5

FIXE.TAILLE.CRAYON : attend un entier en entrée - ne renvoie rien - l'épaisseur du trait du crayon de la tortue est fixée au nombre fourni en entrée.

Exemple :

- FIXE.TAILLE.CRAYON 2 → -

TAILLE.CRAYON : n'attend rien en entrée - renvoie un entier - l'entier renvoyé correspond à l'épaisseur de trait du crayon de la tortue.

Exemple :

- ECRIS TAILLE.CRAYON → 2

INVERSE.CRAYON : n'attend rien en entrée - ne renvoie rien - le crayon écrit dans la couleur complémentaire de la couleur en cours.

Exemple :

- INVERSE.CRAYON → -

GOMME : n'attend rien en entrée - ne renvoie rien - le crayon efface en avançant.

Exemple :

- GOMME → -

Si la couleur de l'écran est modifiée, les traits effacés réapparaîtront.

NORMAL : n'attend rien en entrée - ne renvoie rien - le crayon n'inverse plus et n'efface plus.

Exemple :

- NORMAL → -

ETAT.CRAYON : n'attend rien en entrée - renvoie une liste - la liste renvoyée fournit les données relatives au crayon de la tortue : sa couleur, son épaisseur, s'il écrit ou non, si l'écriture est inversée ou non.

Exemple :

- ECRIS ETAT.CRAYON → 1 1 -1 0

FIXE.CRAYON ou FIXE.ETAT.CRAYON (raccourci : FEC) : attend une liste en entrée - ne renvoie rien - l'état du crayon est fixé selon le contenu de la liste : couleur, épaisseur, écriture ou non, inversion ou non.

Exemple :

- FIXE.ETAT.CRAYON [1 2 -1 0] → -

II-F-2-f - Formes prédéfinies

GVLOGO fournit plusieurs formes prédéfinies qui seront dessinées soit à une position indiquée par l'utilisateur soit par rapport à la position de la tortue. Ces formes ne sont pas orientables suivant un angle (par exemple, celui de l'orientation de la tortue), mais de telles formes sont facilement programmables en LOGO lui-même.

Afin d'éviter un conflit entre le nom des éventuelles procédures définies par l'utilisateur et celui des primitives de dessins de formes, on emploie pour nommer ces dernières le préfixe F_.

F.RECTANGLE : attend une liste en entrée - ne renvoie rien - un rectangle est dessiné avec les coordonnées de deux points fournies en entrée(14).

Exemple :

- F.RECTANGLE [100 100 200 200] → -

F.RECTANGLE.ARRONDI : attend une liste en entrée - ne renvoie rien - un rectangle arrondi est dessiné avec les coordonnées de deux points fournies en entrée.

Exemple :

- F.RECTANGLE.ARRONDI [50 100 150 200] → -

F.CARRE : attend une liste en entrée - ne renvoie rien - un carré est dessiné avec les coordonnées du point supérieur gauche suivies de la longueur de son côté.

Exemple :

- F.CARRE [20 20 50] → -

F.ELLIPSE : attend une liste en entrée - ne renvoie rien - une ellipse est dessinée avec les coordonnées de deux points fournies en entrée.

Exemple :

- F.ELLIPSE [100 100 200 200] → -

F.CERCLE : attend une liste en entrée - ne renvoie rien - un cercle est dessiné avec les coordonnées du premier point du cercle suivies de la longueur de son rayon.

Exemple :

- F.CERCLE [20 20 50] → -

REMPLIS : n'attend rien en entrée - ne renvoie rien - les figures seront pleines et par conséquent recouvriront celles qui seront placées dans leur espace de dessin.

Exemple :

- REMPLIS → -

LAISSE.VOIR : n'attend rien en entrée - ne renvoie rien - les figures seront transparentes et par conséquent ne recouvriront pas celles qui seront placées dans leur espace de dessin.

Exemple :

- LAISSE.VOIR → -

II-F-2-g - Texte avec la tortue

La tortue peut écrire à son emplacement. Le texte aura alors sa couleur et son orientation. L'utilisateur peut aussi écrire à un emplacement défini par lui avec l'orientation, la taille et la couleur de son choix.

TEXTE.TORTUE : attend un mot ou une liste en entrée - ne renvoie rien - le texte spécifié en paramètre est écrit sur l'écran de la tortue en utilisant ses coordonnées, sa taille, sa couleur et son orientation.

Exemple :

- TEXTE.TORTUE [Bonjour mon ami !] → -

FIXE.ANGLE.TEXTE : attend un entier en entrée - ne renvoie rien - le texte sera écrit avec l'angle spécifié en entrée, selon le même principe que l'orientation de la tortue.

Exemple :

- FIXE.ANGLE.TEXTE 90 → -

ANGLE.TEXTE : n'attend rien en entrée - renvoie un entier - la primitive renvoie l'angle actuel d'écriture d'un texte sur l'écran de la tortue.

Exemple :

- ECRIS ANGLE.TEXTE → 90

FIXE.TAILLE.FONTE : attend un entier en entrée - ne renvoie rien - le texte sera écrit avec la taille de caractère spécifiée en entrée, si cette taille est possible pour la police en cours.

Exemple :

- FIXE.TAILLE.FONTE 16 → -

TAILLE.FONTE : n'attend rien en entrée - renvoie un entier - la primitive renvoie la taille actuelle d'écriture d'un texte sur l'écran de la tortue.

Exemple :

- ECRIS TAILLE.FONTE → 16

FIXE.COULEUR.TEXTE (raccourci : FCT) : attend un entier en entrée - ne renvoie rien - le texte sera écrit avec la couleur de caractère spécifiée en entrée.

Exemple :

- FIXE.COULEUR.TEXTE 5 → -

COULEUR.TEXTE : n'attend rien en entrée - renvoie un entier - la primitive renvoie la couleur actuelle d'écriture d'un texte sur l'écran de la tortue.

Exemple :

- ECRIS COULEUR.TEXTE → 5

TEXTE.LIBRE.TORTUE : attend deux entiers puis un mot ou une liste en entrée - ne renvoie rien - le texte indiqué par le mot ou la liste fourni en paramètre est écrit sur l'écran de la tortue en utilisant les entiers comme coordonnées (abscisse et ordonnée). La taille, la couleur et l'orientation sont celles définies par les primitives définies ci-dessus.

Exemple :

- TEXTE.LIBRE.TORTUE 100 150 [Bonjour mon ami !] → -

II-F-3 - Implémentation de la tortue

L'implémentation de la tortue pose trois types de problèmes :

- mathématiques : il faut se plonger dans quelques formules de trigonométrie afin de tenir compte de l'orientation de la tortue pendant ses déplacements ;

- graphiques : il faut gérer un fond indépendant du tracé et une tortue qui doit laisser une trace en se déplaçant à la manière d'un sprite(15) ;

- de communication par événements : une tortue personnalisée sera tributaire des dessins envoyés par l'application tandis que le fond dépendra du contrôle sous-jacent à la surface de la tortue (généralement un panneau). Il est aussi intéressant de notifier tout changement intervenu en rapport avec la tortue, par exemple pour afficher son état dans la barre de statut.

Afin de tenir compte de ces problèmes, l'implémentation met en œuvre une bibliothèque tierce qui ne fonctionne qu'avec Lazarus : BGRABitmap. Cette bibliothèque propose un ensemble d'outils pour créer et manipuler des images avec transparence. Elle est libre de droits et régulièrement mise à jour via le projet LazPaint. Outre les améliorations citées, résolues à travers la gestion d'un canal alpha pour la transparence, elle prend en charge l'anti-alias qui permet de réaliser des dessins de meilleure qualité, sans crénelages désagréables. Le seul point noir est qu'elle ne fonctionne qu'avec Lazarus, mais ceci n'est pas vraiment gênant puisque le projet s'appuie avant tout sur cet EDI.

II-F-3-a - Constantes

L'unité GVConsts n'est modifiée que modestement par l'ajout de quelques constantes :

// *** tortue graphique ***
// couleurs de base
CColors: array[0..19] of TColor = (clBlack, clMaroon, clGreen, clOlive,
clNavy, clPurple, clTeal, clGray, clSilver, clRed, clLime, clYellow,
clBlue, clFuchsia, clAqua, clWhite, clMoneyGreen, clSkyBlue, clCream,
clMedGray);
DgToRad = Pi / 180; // pour les conversions en radians
RadToDg = 180 / Pi; // pour les conversions en degrés
CDefaultScale = 100; // échelle par défaut
CDefaultHeading = 90; // cap par défaut
CDefaultXY = 600; // taille écran tortue par défaut
CDefaultSize = 8; // taille d'une tortue par défaut
CMaxSize = 20; // taille maximale de la tortue
CMaxSpeed = 100; // vitesse maximum de la tortue
CDefaultPenColor = clWhite; // couleur de fond par défaut
CDefaultBackColor = clBlack; // couleur du crayon par défaut
CDefaultPenWidth = 1; // largeur du crayon par défaut

Quelques types viennent compléter le tout afin de définir le type d'écran et de tortue, ainsi qu'un enregistrement possible de l'état complet de la tortue :

type
// *** type d'écrans : enroulement, fenêtre illimitée ou champ clos ***
TScreenTurtle = (teWin, teGate, teRoll);

// *** types de tortue : triangle, dessin ou personnalisée (réservée) ***
TTurtleKind = (tkTriangle, tkPng, tkOwner);

// *** définition d'une tortue ***
TTurtle = record
rSaved: Boolean; // drapeau de sauvegarde
rX: Extended; // abscisse
rY: Extended; // ordonnée
rKind: TTurtleKind; // type de tortue
rSize: Integer; // taille de la tortue
rVisible: Boolean; // drapeau de visibilité
rHeading: Extended; // direction
rPenDown: Boolean; // drapeau de crayon baissé
rPenRubber: Boolean; // drapeau d'effacement
rScaleX: Integer; // échelle des X
rScaleY: Integer; // échelle des Y
rFilled: Boolean; // remplissage
rPenWidth: Integer; // largeur de crayon
rBrush: TBrush; // type de brosse
rPen: TPen; // type de crayon
rFont: TFont; // type de fonte
end;

II-F-3-b - Un peu de mathématiques

Afin de déterminer un déplacement de la tortue sur la surface de travail, il faut connaître ses coordonnées pas à pas en fonction de son cap. Pour cela, on utilisera les fonctions sinus et cosinus :

Aussi bien Lazarus que Delphi fournissent une procédure bien pratique(16) pour les déterminer : SinCos définie pour un réel étendu de la manière suivante :

procedure sincos(theta : float;out sinus;cosinus : float) ;

Theta est l'angle exprimé en radians et les sinus et cosinus sont renvoyés dans les paramètres appropriés.

Voici par exemple comment elle est utilisée dans la procédure Move qui déplace la tortue :

procedure TGVTurtle.Move(Value: Double);
// *** la tortue se déplace ***
var
LSinT, LCosT: Extended;
LTX, LTY: Double;
begin
// calcul du cosinus et du sinus du cap
SinCos((Heading - 90) * DgToRad, LSinT, LCosT);
// calcul des nouvelles coordonnées
LTX := fX - Value * LSinT * (ScaleX / CDefaultScale);
LTY := fY + Value * LCosT * (ScaleY / CDefaultScale);
SetPos(LTX, LTY); // déplacement si possible
end;

fX et fY sont les coordonnées avant le déplacement. On remarquera la transformation des degrés en radians pour l'orientation ainsi que l'utilisation des échelles pour déterminer le nombre réel de pixels à franchir. La position sera par la suite arrondie puisqu'on ne peut dessiner que des pixels à des positions entières.

De la même manière, lorsqu'on voudra dessiner le triangle représentant la tortue, des formules similaires seront utilisées :

procedure TGVTurtle.DrawTriangleTurtle;
// *** dessin de la tortue triangulaire ***
var
LCosT, LSinT: Extended;
LX1, LX2, LX3, LY1, LY2, LY3: Integer;
begin
// on efface la surface
fTtlImg.FillRect(0,0,fWidth, fHeight, BGRAPixelTransparent, dmSet);
// calcul des coordonnées des points de la tortue
SinCos((90 + Heading) * DgToRad, LSinT, LCosT);
LX1 := Round(CoordX + Size * LCosT - LSinT);
LY1 := cY(Round(CoordY + Size * LSinT + LCosT));
LX2 := Round(CoordX - Size * LCosT - LSinT);
LY2 := cY(Round(CoordY - Size * LSinT + LCosT));
LX3 := Round(CoordX - LCosT + (Size shl 1) * LSinT);
LY3 := cY(Round(CoordY - LSinT - (Size shl 1) * LCosT));
with fTtlImg.CanvasBGRA do
begin
if PenColor <> BGRAToColor(BGRAPixelTransparent) then
Pen.Color := PenColor
else
Pen.Color := CDefaultPenColor;
MoveTo(LX1, LY1); // dessin de la tortue
Pen.Width := 2;
LineTo(LX2, LY2);
Pen.Width := 1;
LineTo(LX3, LY3);
LineTo(LX1, LY1);
end;
end;

L'utilisation de shl au lieu d'une multiplication par deux traditionnelle est très efficace pour ce qui est de la vitesse d'exécution.

En ce qui concerne la tortue personnalisée, les données sont tout à fait différentes. Le choix a été fait de partir d'une série de « clichés » de la tortue au format « png », car il supporte très bien la transparence. Malheureusement, les algorithmes qui permettent les rotations sont complexes : c'est pourquoi la solution d'images préenregistrées a été choisie. Elle est par ailleurs plutôt rapide.

procedure TGVTurtle.DrawPNGTurtle;
// *** dessin de la tortue PNG ***
var
LCosT, LSinT: Extended;
LX, LY: Integer;
begin
// on efface la surface
fTtlImg.FillRect(0,0,fWidth, fHeight, BGRAPixelTransparent, dmSet);
BeforeChange; // récupère la bonne image
// calcul des coordonnées de la tortue
SinCos((90 + Heading) * DgToRad, LSinT, LCosT);
LX := Round(CoordX + LCosT - LSinT);
LY := Round(CoordY + LSinT + LCosT);
// copie de la tortue .png
fTtlImg.CanvasBGRA.Draw(LX - (fPNGTurtle.Width shr 1),
cY(LY) - (fPNGTurtle.Height shr 1), fPNGTurtle);
end;

Une autre fonction complexe est Towards qui permet de rendre l'orientation nécessaire de la tortue pour qu'elle pointe vers un point. Elle utilise la fonction mathématique ArcTan qui détermine un angle dont la tangente est donnée.

En voici le listing(17) :

function TGVTurtle.Towards(X, Y: Integer): Double;
// *** renvoie le cap vers un point ***
var
LPX, LPY: Integer;
begin
LPX := Round(CoordX) - X; // calcul des différences entre les points
LPY := Y - Round(CoordY);
Result := 0; // suppose 0
// évalue suivant les calculs
if ((LPX = 0) and (LPY < 0)) then
Result := 270
else if ((LPX = 0) and (LPY > 0)) then
Result := 90
else if ((LPX > 0) and (LPY >= 0)) then
Result := 180 - ArcTan(LPY / LPX) * RadToDg
else if ((LPX < 0) and (LPY > 0)) then
Result := (ArcTan(LPY / Abs(LPX)) * RadToDg)
else if ((LPX < 0) and (LPY <= 0)) then
Result := 360 - (ArcTan(LPY / LPX) * RadToDg)
else if ((LPX > 0) and (LPY < 0)) then
Result := 180 + (ArcTan(Abs(LPY) / LPX) * RadToDg);
end;

Enfin, la fonction Distance permet de calculer la distance entre la tortue et un point donné. Elle fait appel à de simples calculs à partir de triangles rectangles, donc en appliquant le théorème de Pythagore.

La formule bien connue est :

avec xa, ya les coordonnées du premier point et xb, yb celles du second.

La fonction en Pascal donne(18) :

function TGVTurtle.Distance(X, Y: Integer): Double;
// *** renvoie la distance de la tortue à un point donné ***
begin
Result := Sqrt(Sqr(X - CoordX) + Sqr(Y - CoordY));
end;

II-F-3-c - Se déplacer sur l'écran

En dehors de ces quelques notions de mathématiques, le déplacement de la tortue à l'écran pose des problèmes d'affichage. La solution adoptée est celle de la transparence bien maîtrisée par la bibliothèque BGRABitmap.

Sans cette bibliothèque, le comportement de la tortue avec Lazarus était erratique : la transparence fonctionne avec Lazarus dès qu'il s'agit de dessiner une forme simple sur le canevas ; mais si l'on désire déplacer la tortue sur une surface transparente qui prend sa couleur de fond sur un contrôle, le dessin en entier disparaît… du moins, dans la limite de mes connaissances !

Le dessin met en jeu trois couches : la première s'occupe du fond, la deuxième du dessin laissé par la tortue, la troisième par la représentation de cette dernière. On est ainsi assuré que les objets manipulés n'interféreront pas. La bibliothèque fournit en effet les éléments qui permettent de superposer ces couches en gérant la transparence nécessaire.

La méthode centrale se présente ainsi :

function TGVTurtle.GetImg: TBGRABitmap;
// *** récupération de l'image ***
begin
with fActualImg do // on procède par couches
begin
PutImage(0,0,fBckImg,dmDrawWithTransparency); // le fond
PutImage(0,0,fDrwImg,dmDrawWithTransparency); // le dessin
if TurtleVisible then // tortue visible ?
begin
case Kind of
tkTriangle: DrawTriangleTurtle; // on dessine la tortue triangulaire
tkPNG: DrawPNGTurtle; // tortue PNG
end;
PutImage(0,0,fTtlImg,dmDrawWithTransparency); // on l'affiche
end;
end;
Result := fActualImg; // on renvoie l'image
end;

On utilise fBckImg pour afficher le fond, fDrwImg pour le dessin et enfin fTtlImg pour la tortue. Les trois éléments sont du type TBGRABitmap. C'est la bibliothèque BGRABitmap qui s'occupe de fondre ces trois couches afin d'obtenir le dessin souhaité.

On notera aussi que les procédures LineTo et MoveTo adaptent les coordonnées fournies afin de les rendre cohérentes avec un repère dont l'origine est située en bas à gauche de la surface de dessin et non en haut à gauche comme en Pascal.

procedure TGVTurtle.LineTo(X, Y: Double);
// *** déplacement en écrivant ***
begin
fDrwImg.CanvasBGRA.LineTo(Round(X), cY(Round(Y))); // écriture effective
end;

procedure TGVTurtle.MoveTo(X, Y: Double);
// *** déplacement sans écrire ***
begin
fDrwImg.CanvasBGRA.MoveTo(Round(X), cY(Round(Y))); // déplacement effectif
end;

C'est la fonction cY qui est chargée du travail de modification du repère :

function TGVTurtle.cY(Y: Integer): Integer;
// *** change l'ordonnée pour le nouveau repère ***
begin
Result := fHeight - Y; // inversion des ordonnées
end;

Le déplacement proprement dit de la tortue est traité par la méthode DoGo qui prend en compte le type d'écran teRoll, c'est-à-dire l'enroulement. En voici l'implémentation :

procedure TGVTurtle.DoGo(X, Y: Double);
// *** effectue un déplacement de la tortue ***
begin
// si champ clos et hors limites => erreur
if (Screen <> teGate) or IsWithinLimits(X, Y) then
begin
fX := X; // nouvelle abscisse
fY := Y; // nouvelle ordonnée
// ralentit le dessin
Sleep(CMaxSpeed - Speed);
// dessine
if PenDown then  // crayon baissé ?
LineTo(X, Y) // en écrivant
else
MoveTo(X, Y); // sans écrire
Change; // changement notifié
end;
// on continue si l'écran s'enroule
if (Screen = teRoll) and not IsWithinLimits(X,Y) then
begin
// mise à jour des coordonnées après enroulement
// débordement à droite ?
if (X > fWidth) then
begin
MoveTo(0, Y);
X := X - fWidth;
end;
// débordement en bas ?
if (Y > fHeight) then
begin
MoveTo(X, 0);
Y := Y - fHeight;
end;
// débordement à gauche ?
if (X < 0) then
begin
MoveTo(fWidth, Y);
X := fWidth + X;
end;
// débordement en haut ?
if (Y < 0) then
begin
MoveTo(X, fHeight);
Y := fHeight + Y;
end;
fX := X; // nouvelle abscisse
fY := Y; // nouvelle ordonnée
// ralentit le dessin
Sleep(CMaxSpeed - Speed);
// dessine ou déplace suivant l'état du crayon
if PenDown then
LineTo(X, Y)
else
MoveTo(X, Y);
end;
end;

Bien que ce soit une erreur que de vouloir faire sortir la tortue de son espace alors qu'elle est en mode champ clos, GVLOGO se contente de ne pas déplacer la tortue dans ce cas. On pourrait aussi déclencher une exception, mais, d'un point de vue pédagogique, il a semblé plus intéressant de laisser à l'utilisateur le soin de comprendre ce qu'il se passe dans ce cas.

II-F-3-d - Les événements

La récupération de l'image désirée de la tortue est effectuée par le programme qui exploite la classe TGVTurtle. Même si les multiples liens entre les procédures et les unités rendent l'ensemble plutôt complexe, le principe de fonctionnement est simple : quand l'instance de la classe TGVTurtle a besoin de dessiner la tortue, elle exécute la procédure BeforeChange qui envoie en paramètre essentiel l'orientation de la tortue. En retour, elle s'attend à ce que l'unité appelée ou le programme ait affecté la bonne image à sa propriété TurtleImg.

Autrement dit, il s'agit d'une sorte d'aller-retour entre les deux unités : la première envoie un pointeur vers elle-même et l'orientation de la tortue si elle trouve une procédure affectée à son champ privé fOnBeforeChange ; la seconde récupère l'orientation de la tortue, cherche l'image correspondante et l'affecte en retour à la propriété TurtleImg de l'unité génératrice de l'événement.

La méthode BeforeChange est déclarée ainsi, dans la partie protected de la définition :

procedure BeforeChange; // avant le changement

Et voici sa propre définition :

procedure TGVTurtle.BeforeChange;
// *** gestion avant le changement ***
// (permet de mettre à jour une image pour la tortue avant de la dessiner)
begin
if Assigned(fOnBeforeChange) then  // si le gestionnaire existe
fOnBeforeChange(Self, Round(Heading)); // on l'exécute
end;

On voit que la procédure fait elle-même référence au champ privé fOnBeforeChange défini ainsi :

fOnBeforeChange: TTurtleBeforeEvent; // notification avant cap changé

Quant au type TTurtleBeforeEvent, il fournit un squelette de la procédure appelante :

// avant le changement de la tortue
TTurtleBeforeEvent = procedure(Sender: TObject; cHeading: Integer) of object;

Ce squelette est essentiel puisqu'en fournissant un modèle commun il permet aux unités de communiquer entre elles.

Voici à présent comment le programme de test opère pour répondre à la requête de l'unité GVTurtles :

procedure TMainForm.TurtleBeforePaint(Sender: TObject; cHeading: Integer);
// *** image associée à la tortue ***
var
BitM: TBitmap;
begin
// charge l'image de la tortue
BitM := TBitmap.Create;
try
// les images de la tortue sont proposées tous les 5 degrés
ImageListTurtles.GetBitmap(Round(cHeading) div 5, BitM);
// celle qui correspond est assignée au bitmap
GVTurtle.PNGTurtle.Assign(BitM);
finally
BitM.Free;
end;
end;

On remarquera que fOnBeforeChange de l'unité GVTurtles et TurtleBeforePaint du programme de test ont exactement le même en-tête calqué sur TTurtleBeforeEvent. Peu importe que les noms soient différents : ce qui compte, ce sont les paramètres absolument identiques dans leur nombre, leur agencement et leurs types.

Pour que l'ensemble fonctionne, il faut évidemment affecter la procédure appelée au gestionnaire d'événements approprié avec une ligne du genre :

procedure TMainForm.FormCreate(Sender: TObject);
begin
// on crée la tortue
GVTurtle := TGVTurtle.Create(imgTurtle.Width, imgTurtle.Height);
GVTurtle.OnChange := @TurtleState;  // gestionnaire de changement
GVTurtle.OnBeforeChange := @TurtleBeforePaint; // idem avant de dessiner
GVTurtle.ReInit; // initialisation
seSetPosX.Value := imgTurtle.Width shr 1; // position au centre
seSetPosY.Value := imgTurtle.Height shr 1;
end;

Ici, GVTurtle est une instance de la classe TGVTurtle. Cette ligne figure dans la méthode FormCreate de la fiche. Le programme « sait » à présent quelle procédure il doit appeler lorsque l'événement est déclenché.

Contrairement à Delphi, Lazarus exige l'emploi de @ devant la méthode pour l'affecter au gestionnaire d'événements.

Un second événement plus simple est déclenché à chaque modification de la tortue : il s'agit de TTurtleEvent :

type
// changement de la tortue
TTurtleEvent = TNotifyEvent;

Cet événement ne transmet qu'un pointeur vers l'instance de la classe qui a déclenché l'événement, mais permet ainsi d'accéder aux propriétés de la tortue susceptibles d'avoir été modifiées : ses coordonnées, son cap, l'état de sa visibilité, celui de sa capacité à écrire et la couleur d'écriture du crayon. L'intérêt d'une telle notification est, par exemple, de renseigner en temps réel une barre de statut qui affichera en clair ces données pour informer l'utilisateur.

Du côté de l'unité GVTurtles, on retrouve :

- un champ privé qui conservera la procédure affectée si elle existe :

fOnchange: TTurtleEvent; // changement notifié

- une propriété en accès public et renvoyant à ce champ privé :

// événement après le changement de la tortue
property OnChange: TTurtleEvent read fOnchange write fOnchange;

- une procédure dans la partie protected de la définition de la classe, appelée quand nécessaire et en charge d'appeler elle-même la procédure de l'unité qui exploite la tortue :

procedure Change; // gestion du changement

- la définition de cette procédure :

procedure TGVTurtle.Change;
// *** notification de changement ***
begin
if Assigned(fOnchange) then // on exécute le gestionnaire s'il existe
fOnchange(Self);
end;

Il ressemble tout à fait au gestionnaire vu précédemment : si le champ privé fOnChange est affecté, on exécute la méthode à laquelle il renvoie.

Du côté du programme de test, on retrouve :

- la déclaration de la procédure qui sera appelée en se conformant au squelette fourni par TTurtleEvent :

public
// message de la tortue
procedure TurtleState(Sender: TObject);

- sa définition qui affiche les données fournies sur la barre de statut :

procedure TMainForm.TurtleState(Sender: TObject);
// état de la tortue
begin
GVTurtle.TurtleBitmap.Draw(imgTurtle.Canvas,0,0);
imgTurtle.Invalidate;
// données de la tortue
with GVTurtle do
statusbar.Panels[1].Text := Format('X: %.3d Y: %.3d Cap: %.3d',
[Round(CoordX), Round(CoordY), Round(Heading)]) +
' Visible: ' + IfThen(TurtleVisible, P_True, P_False) +
' Baissé: ' + IfThen(PenDown, P_True, P_False);
cbPenColor.Selected := GVTurtle.PenColor; // couleur du crayon
cbScreenColor.Selected := GVTurtle.ScreenColor; // couleur du fond
end;

- afin de créer le lien entre les deux unités, son affectation lors de la création de la fiche :

procedure TMainForm.FormCreate(Sender: TObject);
begin
// on crée la tortue
GVTurtle := TGVTurtle.Create(imgTurtle.Width, imgTurtle.Height);
GVTurtle.OnChange := @TurtleState;  // gestionnaire de changement
GVTurtle.OnBeforeChange := @TurtleBeforePaint; // idem avant de dessiner
GVTurtle.ReInit; // initialisation
seSetPosX.Value := imgTurtle.Width shr 1; // position au centre
seSetPosY.Value := imgTurtle.Height shr 1;
end;

On a là un cas très classique de gestion d'événements, avec toutes les étapes de sa mise en place. Le code source de l'unité est alors émaillé d'appels à Change lorsqu'intervient un changement concernant la tortue.

II-F-3-e - La classe TGVTURTLE

Voici l'interface de la copieuse classe TGVTurtle :

TGVTurtle = class(TObject)
strict private
fError: TGVErrors; // gestion des erreurs
// *** la tortue ***
fX: Double; // abscisse
fY: Double; // ordonnée
fHeading: Double; // cap
fSize: Integer; // taille
fTurtleKind: TTurtleKind; // type
fTurtleVisible: Boolean; // visibilité
fSpeed: Integer; // vitesse
fPNGTurtle: TBGRABitmap; // PNG en cours
fSavedTurtle: TTurtle; // sauvegarde
fOnchange: TTurtleEvent; // changement notifié
fOnBeforeChange: TTurtleBeforeEvent; // notification avant cap changé
// *** le crayon ***
fPenDown: Boolean; // levé/baissé
fPenRubber: Boolean; // gomme
fPenWidth: Integer; // largeur
fPenColor: TColor; // couleur
fTempColor: TColor; // sauvegarde temporaire de la couleur d'écriture
// *** l'écran ***
fScreen: TScreenTurtle; // mode
fHeight: Integer; // hauteur de la surface d'origine
fWidth: Integer; // largeur de la surface d'origine
fScaleX: Integer; // échelle des X
fScaleY: Integer; // échelle des Y
fScreenColor: TColor; // couleur
fBckImg: TBGRABitmap; // fond
fDrwImg: TBGRABitmap; // surface dessinée
fTtlImg: TBGRABitmap; // dessin de la tortue
fActualImg: TBGRABitmap; // surface réelle
fFilled: Boolean; // drapeau de remplissage
// *** méthodes internes des propriétés ***
function GetCoordX: Double;
function GetCoordY: Double;
function GetImg: TBGRABitmap;
function GetLocalPenColor: Integer;
procedure SetLocalPenColor(AValue: Integer);
function GetLocalScreenColor: Integer;
procedure SetLocalScreenColor(AValue: Integer);
procedure SetCoordX(AValue: Double);
procedure SetCoordY(AValue: Double);
procedure SetFilled(AValue: Boolean);
procedure SetHeading(AValue: Double);
procedure SetPenColor(AValue: TColor);
procedure SetPenDown(AValue: Boolean);
procedure SetPenWidth(AValue: Integer);
procedure SetScreen(AValue: TScreenTurtle);
procedure SetScreenColor(AValue: TColor);
procedure SetSize(AValue: Integer);
procedure SetSpeed(AValue: Integer);
procedure SetTurtleKind(AValue: TTurtleKind);
procedure SetTurtleVisible(AValue: Boolean);
procedure SetRubberPen(AValue: Boolean);
// *** états ***
function GetTurtleState: string; // état de la tortue
procedure SetTurtleState(const St: string);
function GetPenState: string; // état du crayon
procedure SetPenState(const St: string);
function GetPosState: string; // position du crayon
procedure SetPosState(const St:string);
function GetScaleState: string; // état de l'échelle
procedure SetScaleState(const St: string);
function GetScreenState: string; // état de l'écran
procedure SetScreenState(const St: string);
// *** méthodes internes générales ***
function IsWithinLimits(X, Y: Double): Boolean; // dans limites ?
procedure DoGo(X, Y: Double); // effectue un déplacement
procedure LineTo(X, Y: Double); // déplacement en écrivant
procedure MoveTo(X, Y: Double); // déplacement sans écrire
// *** figures prédéfinies générales ***
// arc d'ellipse (b : début, e : fin - en degrés)
procedure ArcAntialias(x, y, rx, ry: single; b, e: word;
c: TBGRAPixel; w: single);
// arc d'ellipse rempli (b : début, e : fin - en degrés)
procedure FillArcAntiAlias(x, y, rx, ry: single; b, e: word;
c: TBGRAPixel);
// portion d'ellipse (b : début, e : fin - en degrés)
procedure PieAntialias(x, y, rx, ry: single; b, e: word;
c: TBGRAPixel; w: single);
// portion d'ellipse remplie (b : début, e : fin - en degrés)
procedure FillPieAntiAlias(x, y, rx, ry: single; b, e: word;
c: TBGRAPixel);
// *** fonctions utiles ***
function ColorToIntColor(N: TColor): Integer; // couleur en couleur locale
function IntColorToColor(N: Integer): TColor; // couleur locale en couleur
protected
function cY(Y: Integer): Integer; virtual; // ordonnée dans nouveau repère
procedure DrawTriangleTurtle; virtual; // tortue triangulaire
procedure DrawPNGTurtle; virtual; // tortue PNG
procedure Change; // gestion du changement
procedure BeforeChange; // avant le changement
public
constructor Create(Width, Height: Integer); // création
destructor Destroy; override; // destructeur
procedure ReInit; // réinitialisation de l'objet
procedure PenReverse; // inversion de l'écriture
procedure Move(Value: Double); // la tortue se déplace
procedure SetPos(X, Y: Double); // fixe les coordonnées de la tortue
procedure Turn(Value: Double); // la tortue tourne
procedure Home; // tortue à l'origine
procedure Wipe; // nettoyage de l'écran
procedure SaveTurtle; // sauvegarde de la tortue
procedure ReloadTurtle(Clean: Boolean); // récupère une tortue sauvée
// renvoie le cap vers un point
function Towards(X, Y: Integer): Double; overload;
function Towards(const St: string): Double; overload;
// renvoie la distance de la tortue à un point donné
function Distance(X, Y: Integer): Double; overload;
function Distance(const St: string): Double; overload;
// *** gestion du texte ***
// texte affiché sur l'écran de la tortue
procedure Text(const St: string; X,Y, Angle: Integer); overload;
// texte affiché à l'emplacement de la tortue
procedure Text(const St: string); overload;
// *** figures prédéfinies ***
// dessine un rectangle
procedure Rectangle(X1, Y1, X2, Y2: Integer); overload;
// dessine un rectangle à l'emplacement de la tortue
procedure Rectangle(X2, Y2: Integer); overload;
// dessine un carré
procedure Square(X1, Y1, L: Integer); overload;
// dessine un carré à l'emplacement de la tortue
procedure Square(L: Integer); overload;
// dessine un rectangle arrondi
procedure RoundRect(X1, Y1, X2, Y2: Integer); overload;
// dessine un rectangle arrondi à l'emplacement de la tortue
procedure RoundRect(X2, Y2: Integer); overload;
// dessine une ellipse
procedure Ellipse(X1, Y1, X2, Y2: Integer); overload;
// dessine une ellipse à l'emplacement de la tortue
procedure Ellipse(X2, Y2: Integer); overload;
// dessine un cercle
procedure Circle(X1, Y1, R: Integer); overload;
// dessine un cercle à l'emplacement de la tortue
procedure Circle(R: Integer); overload;
// dessine un arc d'ellipse
procedure Arc(X1, Y1, X2, Y2, X3, Y3: Integer); overload;
// dessine un arc d'ellipse à l'emplacement de la tortue
procedure Arc(X2, Y2, X3, Y3: Integer); overload;
// dessine une section d'ellipse
procedure Pie(X1, Y1, X2, Y2, X3, Y3: Integer); overload;
// dessine une section d'ellipse à l'emplacement de la tortue
procedure Pie(X2, Y2, X3, Y3: Integer); overload;
// *** propriétés ***
// abscisse de la tortue
property CoordX: Double read GetCoordX write SetCoordX;
// ordonnée de la tortue
property CoordY: Double read GetCoordY write SetCoordY;
// type de tortue
property Kind: TTurtleKind read fTurtleKind write SetTurtleKind
default tkTriangle;
// visibilité de la tortue
property TurtleVisible: Boolean read fTurtleVisible write SetTurtleVisible
default True;
// direction de la tortue
property Heading: Double read fHeading write SetHeading;
// taille de la tortue
property Size: Integer read fSize write SetSize default CDefaultSize;
// drapeau d'écriture
property PenDown: Boolean read fPenDown write SetPenDown default True;
// type de zone de déplacement
property Screen: TScreenTurtle read fScreen write SetScreen
default teWin;
// échelle des X
property ScaleX: Integer read fScaleX write fScaleX default CDefaultScale;
// échelle des Y
property ScaleY: Integer read fScaleY write fScaleY default CDefaultScale;
property PenRubber: Boolean read fPenRubber write SetRubberPen
default False; // état de la gomme
property PenColor: TColor read fPenColor write SetPenColor
default CDefaultPenColor; // couleur du crayon
property LocalPenColor: Integer read GetLocalPenColor
write SetLocalPenColor; // couleur du crayon en couleur locale
property PenWidth: Integer read fPenWidth write SetPenWidth default
CDefaultPenWidth; // largeur du crayon
// état du remplissage
property Filled: Boolean read fFilled write SetFilled default True;
// vitesse de dessin de la tortue
property Speed: Integer read fSpeed write SetSpeed default CMaxSpeed;
property ScreenColor: TColor read fScreenColor write SetScreenColor
default CDefaultBackColor; // couleur du fond d'écran
property LocalScreenColor: Integer read GetLocalScreenColor
write SetLocalScreenColor; // couleur de l'écran en couleur locale
// événement après le changement de la tortue
property OnChange: TTurtleEvent read fOnchange write fOnchange;
// événement avant le changement de la tortue
property OnBeforeChange: TTurtleBeforeEvent read fOnBeforeChange
write fOnBeforeChange;
// surface de dessin de la tortue
property TurtleBitmap: TBGRABitmap read GetImg;
// tortue PNG
property PNGTurtle: TBGRABitmap read fPNGTurtle write fPNGTurtle;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
// état de la tortue
property TurtleState: string read GetTurtleState write SetTurtleState;
// état du crayon
property PenState: string read GetPenState write SetPenState;
// position du crayon
property PosState: string read GetPosState write SetPosState;
// échelle
property ScaleState: string read GetScaleState write SetScaleState;
// écran
property ScreenState: string read GetScreenState write SetScreenState;
end;

En dehors des aspects déjà étudiés, la classe ne présente pas de problèmes majeurs. La tortue fait bien sûr appel à des propriétés afin que l'utilisateur puisse en maîtriser l'aspect et le comportement. L'interface est aussi alourdie par de nombreuses fonctions de dessin : une première version dessine en se référant à des coordonnées traditionnelles tandis qu'une seconde version surchargée grâce à overload utilise les coordonnées de la tortue pour amorcer son dessin.

On notera que les coordonnées sont des réels alors que la position finale est un entier : cela permet de calculer au plus près la position de la tortue avant de l'arrondir, évitant ainsi des approximations cumulées.

II-F-3-f - Test de l'unité GVTurtles

Comme toujours, le programme de test est décliné en deux versions :

- Lazarus pour Windows 32 ;

- Lazarus pour Linux.

L'utilisateur pourra tester la plupart des fonctions et propriétés définies pour manipuler la tortue. Le programmeur sera plus intéressé par le mécanisme des notifications d'événements tel que décrit plus haut.

Il faut avoir installé la bibliothèque BGRABitmap pour utiliser l'unité GVTurtles !

Pour avoir une idée des possibilités de dessin de la tortue, un bouton marqué « dessin » dessine une série de 36 carrés en rosace. Afin de se rendre compte de la rapidité de la tortue triangulaire par rapport à celle graphique, il est judicieux d'utiliser ce bouton spécial dans trois configurations : graphique, triangle et tortue cachée.

On remarquera aussi quatre boutons : le premier met en œuvre la méthode Text dans sa version indépendante de la tortue tandis que le second la teste avec des coordonnées fournies par cette même tortue ; le troisième illustre ce que peut réaliser la méthode Text avec une fleur de mots en couleurs ; enfin, le dernier, accompagné d'un TSpinEdit, contrôle l'épaisseur du trait de la tortue lorsqu'elle dessine. De plus, trois boutons TRadioEdit permettent de contrôler le type de champ de la tortue : illimité avec teWin (valeur par défaut), limité par les bords de l'écran (teGate) et enfin enroulé (teRoll).

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

Avant d'aborder les outils de programmation et de rentrer dans le cœur de l'interpréteur, une récréation s'impose avec la création d'un petit logiciel de dessin baptisé EasyTurtle.

III-A - Le projet EasyTurtle

Il s'agit de mettre en œuvre l'unité GVTurtles en permettant à un enfant de dessiner avec la tortue, de rejouer l'ensemble des ordres qu'il lui aura donnés, ou encore de charger et de sauvegarder ses réalisations.

Quant au programmeur, il pourra utiliser quelques outils particulièrement utiles, en particulier les listes d'actions.

III-B - B - Mode d'emploi rapide

III-B-1 - L'écran d'accueil

L'écran d'accueil se présente comme ceci :

Sur l'essentiel de la fenêtre se situe la zone d'affichage de la tortue : c'est ici que seront réalisés les dessins. Afin de faciliter sa manipulation, la zone de dessin est en mode « clos », ce qui signifie que la tortue ne peut pas être perdue de vue, car tout ordre tendant à la faire disparaître conduira à la faire buter contre le bord de son champ comme s'il y avait une barrière.

À droite de cette zone d'affichage, on aperçoit toute une série de boutons : ce sont les actions mises à disposition de l'utilisateur.

III-B-2 - La tortue

On distingue ainsi les actions concernant la tortue :

La tortue peut donc avancer, reculer, tourner à gauche et à droite. Chacune de ces opérations se fait selon une valeur par défaut modifiable grâce à une fenêtre de réglage des préférences.

Si elle est de forme triangulaire, la tortue peut aussi grossir et rapetisser. Dans le cas contraire, ces boutons sont grisés, et par conséquent inactifs.

L'utilisateur peut effacer l'écran, renvoyer la tortue à son origine, l'autoriser ou lui interdire d'écrire, la rendre visible ou invisible.

Un bouton indique toujours l'action à réaliser, et non l'état de la tortue. Ainsi, si le bouton comporte le message « N'écris plus », c'est que la tortue laisse actuellement une trace et qu'une pression sur le bouton lui demandera de ne plus écrire.

III-B-3 - Couleurs et formes

Le panneau suivant s'occupe des couleurs et des formes :

Le premier bouton permute l'apparence de la tortue, entre le triangle et le dessin au format png. Le second ouvre une fenêtre de choix de la couleur du crayon, tout comme le suivant pour la couleur de fond. Les deux derniers dessinent respectivement un carré et un cercle à l'emplacement de la tortue.

III-B-4 - Ordres généraux

Le panneau suivant regroupe les ordres généraux concernant le travail effectué par la tortue :

EasyTurtle propose un enregistrement des ordres donnés à la tortue. C'est à partir de cet enregistrement qu'opéreront les boutons, « Charge », « Sauve », « RAZ » et « Défais ».

L'utilisateur peut sauvegarder et charger son travail. Il peut ouvrir des fenêtres spécialisées : « Outils », « À Propos » et « Aide ». Il peut aussi rejouer une séquence enregistrée et interrompre cette répétition : ce sont les deux boutons sans légende qui apparaissent en bas à gauche de la copie d'écran. Il peut encore annuler le dernier ordre donné à la tortue (bouton « Défais ») ou remettre à zéro toute la séquence (bouton « RAZ »). Enfin, il peut quitter le logiciel.

Le fait de charger une suite d'ordres l'exécute immédiatement après le chargement.

Suivant l'état du logiciel, certains boutons seront désactivés : par exemple, les boutons « Défais », « Sauve » et « Rejoue » ne seront activés que si des ordres ont été enregistrés. Le bouton « Stop » ne sera activé que si le bouton « Rejoue » a été pressé et seulement le temps de la répétition. Lorsque le bouton « Rejoue » a été pressé, tous les boutons, sauf « Quitter » et « Stop », sont désactivés.

Pour réinitialiser la séquence d'ordres, plusieurs possibilités sont offertes : chargement d'un nouveau fichier d'ordres, pression sur le bouton « RAZ » et modification du fond de l'écran. Contrairement au bouton « Efface l'écran », le bouton « RAZ » conserve la couleur d'écriture et celle du fond de l'écran.

III-B-5 - L'aide

Une aide peut être demandée :

III-B-6 - Boîte « À propos »

De même, on peut accéder à une boîte « À propos » :

III-B-7 - Boîte des préférences

Enfin, une boîte d'outils permet de modifier les valeurs par défaut de certains ordres concernant le dessin effectué par la tortue :

Les modifications apportées grâce à la boîte des préférences sont enregistrées avec le fichier des ordres. Les valeurs sont données en pixels et en degrés.

III-B-8 - Autres éléments

Tous les boutons décrits ci-dessus ont leur double dans la barre d'outils située en haut de la fenêtre principale :

Les pictogrammes sont évidemment les mêmes, afin de renforcer la cohérence du logiciel. De plus, la plupart des ordres peuvent être donnés par une combinaison de touches indiquée en aide ponctuelle près du bouton avant de le presser et dans la barre de statut.

Pour terminer cette présentation rapide, il faut noter des indicateurs fournis par la barre de statut et par deux disques situés en bas à droite de la fenêtre principale :

On prend ainsi connaissance d'une aide succincte concernant le bouton survolé par la souris, des principales valeurs associées à la tortue et des valeurs attribuées à la couleur du crayon de la tortue (premier disque) et au fond de l'écran (second disque), ainsi que l'état du logiciel : « enregistrement en cours », « sauvegarde en cours », « chargement en cours » et « répétition des ordres ».

III-C - La programmation

La suite de cette partie décrit le fonctionnement d'EasyTurtle. Contrairement aux autres logiciels d'exemples, celui-ci fonctionne à partir de plusieurs fiches :

L'unité principale s'appelle Main.pas. GVAbout.pas contient la boîte « À propos », Help.pas l'aide et GVTools.pas la boîte des préférences.

L'appel d'une fiche externe se fait selon un modèle courant depuis la fiche « Main ». Voici, par exemple, l'appel de la boîte « À propos » :

procedure TMainForm.ActionAboutExecute(Sender: TObject);
// *** boîte à propos ***
begin
GVAbout.AboutForm := TAboutForm.Create(Self); // on crée la fiche
try
GVAbout.AboutForm.ShowModal; // on l'affiche
finally
GVAbout.AboutForm.Free; // on la libère
end;
end;

III-C-1 - La fiche principale

La fiche « Main » contient toutes les méthodes nécessaires au fonctionnement d'EasyTurtle. Les éléments les plus complexes sont ceux relatifs à la mémorisation des ordres donnés à la tortue : le logiciel utilise à cette fin un tableau ouvert géré par une méthode nommée Memorize :

procedure TMainForm.Memorize(const Value: Integer);
// *** mémorisation d'une action ***
begin
SetLength(MemoInt, Length(MemoInt) + 1); // on augmente la taille du tableau
MemoInt[Length(MemoInt) - 1] := Value; // on enregistre
pSaved := False; // séquence non enregistrée
end;

Cette méthode ajuste la taille du tableau avant d'enregistrer la nouvelle donnée. Elle indique aussi que la séquence a été modifiée en vue d'un futur enregistrement.

La plupart des ordres sont gérés de manière identique. Voici par exemple l'ordre ActionForwardExecute qui fait avancer la tortue :

procedure TMainForm.ActionForwardExecute(Sender: TObject);
// *** la tortue avance ***
begin
GVTurtle.Move(pForward); // la tortue bouge
Memorize(CT_Forward); // mémorisation
end;

On exécute l'ordre et on l'enregistre, rien de plus facile ! Simplement, afin de permettre aux boutons, à la barre d'outils et aux combinaisons de touches de fonctionner de la même manière sans dupliquer le code, on utilise un composant TActionList qui centralise les actions.

En plus de l'exécution, on a prévu une mise à jour (disponibilité, visibilité, affichage) de chaque fonction suivant l'état du logiciel :

procedure TMainForm.ActionForwardUpdate(Sender: TObject);
// actions actives/inactives
begin
// seulement si enregistrement
(Sender as TAction).Enabled := (pState = stRecording);
end;

Ici, l'objet appelant (qui doit être une action) n'est activé que si le mode est celui de l'enregistrement. En effet, il ne faut pas continuer à dessiner au cours de la sauvegarde, du chargement ou si l'on est en train de rejouer toute la séquence.

On aurait pu indiquer directement ActionForward dans la méthode, mais le transtypage (Sender as TAction) permet de partager le même gestionnaire avec d'autres actions au comportement identique (ActionBackward, par exemple).

Rejouer la séquence exige de dispatcher les ordres en fonction de leur enregistrement :

procedure TMainForm.Replay;
// *** rejoue les actions ***
begin
GVTurtle.ReInit; // réinitialisation de la tortue
GVTurtle.Speed := TbSpeed.Position; // vitesse selon barre
GVTurtle.Screen := teGate; // écran clos
fCmd := C_MinCmds; // on pointe sur le premier élément du tableau hors en-tête
pForward := MemoInt[1]; // on récupère les données de l'en-tête
pBackward := MemoInt[2];
pLeft := MemoInt[3];
pRight := MemoInt[4];
pLength := MemoInt[5];
GVTurtle.ScreenColor := MemoInt[6];
// on boucle tant qu'il y a des ordres et qu'un arrêt n'a pas été demandé
while (fCmd < Length(MemoInt)) and (pState = stPlaying) do  // on balaie le tableau
begin
//images adaptées pour les roues
tbReplay.ImageIndex := (fCmd mod 31) + 15;
ImageListBigWait.GetBitmap(fCmd mod 31 ,ImgRound.Picture.Bitmap);
// on permet aux messages d'être traités
Application.ProcessMessages;
// on répartit le travail suivant les ordres enregistrés
case MemoInt[fCmd] of
CT_Forward : GVturtle.Move(pForward); // avance
CT_Backward : GVturtle.Move(pBackward); // recule
CT_Left : GVturtle.Turn(pLeft); // à gauche
CT_Right : GVturtle.Turn(pRight); // à droite
CT_Bigger : GVturtle.Size := GVTurtle.Size + 2;  // taille + 2
CT_Minus : GVturtle.Size := GVTurtle.Size - 2; // taille - 2
CT_Home :  begin
GVTurtle.Home; // maison
Inc(fCmd, 3); // ordre suivant
end;
CT_UpDown : GVTurtle.PenDown := not GVTurtle.PenDown; // crayon baissé
// visibilité
CT_SeeSaw : GVTurtle.TurtleVisible := not GVTurtle.TurtleVisible;
CT_Kind : if GVTurtle.Kind <> tkTriangle then // type
GVTurtle.Kind := tkTriangle
else
GVTurtle.Kind := tkPng;
CT_Pen : begin  // couleur crayon
Inc(fCmd,2); // ordre suivant
GVTurtle.PenColor := MemoInt[fCmd];
end;
CT_Square: GVTurtle.Square(pLength); // carré
CT_Circle: GVTurtle.Circle(pLength); // cercle
end;
Inc(fCmd); // ordre suivant
end;
Refresh; // remet à jour les voyants
pState := stRecording; // on repasse en mode enregistrement
end;

Le champ privé fCmd est le pointeur utilisé sur l'ordre en cours. Avant de rejouer les ordres, on s'occupe de l'en-tête qui contient les valeurs modifiables depuis la fenêtre des paramètres. Ces valeurs sont enregistrées avec l'éventuel fichier de sauvegarde.

On remarquera la présence de Application.ProcessMessages qui permet à l'application de réagir aux événements tels que l'appui sur le bouton « Stop ». On profite aussi de cette boucle pour animer certains boutons : ainsi, deux roues seront animées. Certaines données ne sont utiles qu'en cas d'action de correction, en remontant dans le temps : pour rejouer la séquence, on les ignore simplement (voir le traitement de CT_Home, par exemple).

La partie la plus complexe de cette unité est celle relative à la fonction « Défaire » :

procedure TMainForm.ActionUndoExecute(Sender: TObject);
// *** défaire la dernière action ***
begin
if (Length(MemoInt) > (C_MinCmds + 4)) and // ordre le plus long = HOME
(MemoInt[Length(MemoInt) - 4] = CT_Home) then
begin
GVTurtle.PenRubber := True;  // on efface
GVTurtle.SetPos(Round(MemoInt[Length(MemoInt) - 2])
, Round(MemoInt[Length(MemoInt) - 1])); // on se repositionne
GVTurtle.Heading := MemoInt[Length(MemoInt) - 3]; // direction de la tortue
GVTurtle.PenRubber := False; // on écrit normalement
SetLength(MemoInt, Length(MemoInt) - 4); // on réajuste la mémorisation
end
else
if (Length(MemoInt) > (C_MinCmds + 3)) and (MemoInt[Length(MemoInt) - 3]
= CT_Pen) then // couleur de crayon
begin
GVTurtle.PenColor := MemoInt[Length(MemoInt) - 2]; // couleur récupérée
SetLength(MemoInt, Length(MemoInt) - 3); // on ajuste
end
else
begin
case MemoInt[Length(MemoInt) - 1] of
CT_Forward: begin  // on a avancé
GVTurtle.PenRubber := True; // on efface
GVTurtle.Move(-pForward); // donc on recule
GVTurtle.PenRubber := False; // on écrit normalement
end;
CT_Backward: begin // on a reculé
GVTurtle.PenRubber := True; // on efface
GVTurtle.Move(-pBackward); // donc on avance
GVTurtle.PenRubber := False; // on écrit normalement
end;
CT_Left: GVTurtle.Turn(-pLeft); // gauche devient droite
CT_Right: GVTurtle.Turn(-pRight); // droite devient gauche
CT_SeeSaw: GVTurtle.TurtleVisible := not GVTurtle.TurtleVisible; // visibilité
CT_UpDown: GVTurtle.PenDown:= not GVTurtle.PenDown; // écriture ou non
CT_Kind: if GVTurtle.Kind = tkTriangle then // type de tortue
GVTurtle.Kind := tkPNG
else
GVTurtle.Kind := tkTriangle;
CT_Bigger: GVTurtle.Size := GVTurtle.Size - 2; // on a grossi
CT_Minus: GVTurtle.Size := GVTurtle.Size + 2; // on a rapetissé
CT_Circle: begin  // un cercle
GVTurtle.PenRubber:= True;
GVTurtle.Circle(pLength+1);
GVTurtle.PenRubber:= False;
end;
CT_Square: begin  // un carré
GVTurtle.PenRubber:= True;
GVTurtle.Square(pLength+1);
GVTurtle.PenRubber:= False;
end;
end;
SetLength(MemoInt, Length(MemoInt) - 1); // on ajuste la mémorisation
end;
pSaved := not (Length(MemoInt) > C_MinCmds); // drapeau d'enregistrement
end;

En effet, pour être annulées, certaines fonctions exigent que l'état de la tortue soit enregistré au préalable : ainsi, pour annuler un retour à l'origine, il faut se souvenir de l'ancien emplacement de la tortue, mais aussi de son orientation. En amont, il faut donc que chaque ordre mémorise ce qu'attendra une éventuelle correction.

La solution adoptée ici est de repasser sur le dernier trait en l'effaçant. Cette méthode est très rapide, mais elle peut effacer un trait qui devrait être conservé parce qu'il est recouvrant.

La méthode ActionSaveExecute d'enregistrement de la séquence doit tenir compte des remarques précédentes :

procedure TMainForm.ActionSaveExecute(Sender: TObject);
// *** sauvegarde des ordres de la tortue ***
var
F: TextFile;
OK: Boolean;
I: Integer;
begin
OK := False; // abandon par défaut
repeat
Ok := SaveDialog.Execute; // dialogue de sauvegarde
if Ok then
begin
// confirmation si le fichier existe
if FileExists(SaveDialog.FileName) then  // boîte de dialogue si existe
// demande de remplacement si le fichier existe
case MessageDlg(Format(ME_Replace,[ExtractFileName(SaveDialog.FileName)]),
mtConfirmation, mbYesNoCancel,0) of
mrYes: Ok := True; // on écrase l'ancien fichier
mrNo: Ok := False; // on recommence
mrCancel: Exit; // abandon si le fichier existe
end;
end
else
Exit; // abandon dès la boîte de dialogue
until Ok;
try
AssignFile(F,SaveDialog.FileName); // fichier assigné
pState := stSaving; // mode sauvegarde indiqué
try
Rewrite(F); // on remet à zéro le fichier
for I := 0 to Length(MemoInt) - 1 do // on balaie les ordres enregistrés
begin
// images qui tournent pendant le chargement
tbReplay.ImageIndex := (I mod 31) + 15;
ImageListBigWait.GetBitmap(I mod 31 ,ImgRound.Picture.Bitmap);
Application.ProcessMessages; // on traite les messages
writeln(F, MemoInt[I]); // on écrit dans le fichier
end;
pSaved := True; // sauvegarde effectuée
except
// erreur de sauvegarde
MessageDlg(Format(ME_SaveError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk],0);
end;
finally
CloseFile(F); // fermeture du fichier
pState := stRecording;
Refresh;  // remet à jour les voyants
end;
end;

Cette méthode vérifie l'existence du fichier avant d'éventuellement l'écraser puis enregistre les données dans l'ordre du tableau des commandes. Elle anime aussi deux roues en permettant à l'application de gérer les événements grâce à la méthode Application.ProcessMessages.

Le chargement d'un fichier est un brin plus complexe :

procedure TMainForm.ActionLoadExecute(Sender: TObject);
// ouverture d'un fichier de commandes
var
F: TextFile;
Num, I: Integer;
begin
if OpenDialog.Execute then // boîte de dialogue d'ouverture de fichier
begin
try
AssignFile(F,OpenDialog.FileName); // fichier assigné
pState := stLoading;  // statut signifié
try
Reset(F); // fichier réinitialisé
readln(F, Num);
if Num <> CT_Version then
begin
MessageDlg(Format(ME_VersionError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk], 0); // signale une erreur de version
Exit; // sortie
end;
readln(F, Num); // récupère les données de l'en-tête
pForward := Num;
readln(F, Num);
pBackward := Num;
readln(F, Num);
pLeft := Num;
readln(F, Num);
pRight := Num;
readln(F, Num);
pLength := Num;
readln(F, Num);
GVTurtle.ScreenColor := Num;
ActionRAZExecute(Sender); // initialisation
I := -1; // pointeur de travail pour l'affichage
repeat
Inc(I); // élément suivant
// images des roues mises à jour
tbReplay.ImageIndex := (I mod 31) + 15;
ImageListBigWait.GetBitmap(I mod 31 ,ImgRound.Picture.Bitmap);
Application.ProcessMessages; // on permet les messages
readln(F, Num); // on lit une donnée
Memorize(Num); // qu'on enregistre
until EOF(F) ; // jusqu'à la fin du fichier
except
MessageDlg(Format(ME_LoadError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk], 0); // signale une erreur de lecture
ActionEraseExecute(Sender); // on remet à zéro
end;
finally
CloseFile(F); // ferme le fichier
pState := stRecording; // on enregistre de nouveau
Refresh;  // remet à jour les voyants
end;
ActionReplayExecute(Sender); // rejoue immédiatement la séquence
pSaved := True; // enregistrement déjà fait
end;
end;

Le premier élément du fichier doit contenir le numéro de version actuelle, soit 100. Il est suivi de l'en-tête puis des données proprement dites. Au fur et à mesure de la lecture, on fait tourner les roues habituelles et on enregistre les ordres grâce à la méthode Memorize. À la fin de la méthode, on lance l'exécution de la séquence afin d'avoir un écran à jour : sans ce dernier point, la fonction « Défaire » serait erronée puisqu'elle corrigerait un écran non dessiné !

On peut enfin mentionner la méthode CloseQuery :

procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
// *** demande de fermeture ***
begin
// fermeture à confirmer
if not pSaved then // si séquence non enregistrée
begin  // demande d'enregistrement
case MessageDlg(ME_NotSaved, mtConfirmation, mbYesNoCancel,0) of
mrYes: begin // oui
ActionSaveExecute(Sender); // sauvegarde
CanClose := pSaved; // on sort si c'est fait
end;
mrNo: CanClose := True; // on sort
mrCancel: CanClose := False; // on ne sort pas
end;
end
else // cas où il n'y a rien à enregistrer
CanClose := (MessageDlg(ME_Close, mtConfirmation, mbYesNo,0) = mrYes);
// on arrête le dessin si nécessaire
if CanClose then
pState := stRecording;
end;

Lors d'une demande de fermeture du logiciel, on vérifie préalablement si la séquence en cours, lorsqu'elle a été modifiée, doit être enregistrée. Les messages diffèrent suivant les cas.

Le test de CanClose qui modifie si nécessaire pState s'explique par le fait que l'utilisateur peut avoir demandé la fermeture du logiciel alors qu'on rejoue une séquence. On interrompt alors cette répétition et l'on ferme le logiciel.

III-C-2 - Les autres fiches

La boîte des préférences comprise dans l'unité GVTools est d'une simplicité enfantine : elle modifie ou non les données qu'elle doit gérer suivant le bouton pressé.

L'unité Help permet d'introduire quelques animations très simples : une image suit le déplacement de la souris en modifiant son champ Left, les composants TLabel ont leur texte en gras ou non suivant l'entrée/la sortie de la souris de leur surface…

L'unité GVAbout comprend aussi une animation aléatoire de la tortue.

III-C-3 - Un logiciel tout en français…

Un programmeur non averti peut être surpris par le comportement de Lazarus dès qu'il s'agit de quitter les sentiers de l'anglais.

En effet, un programme aussi simple que celui-ci pose problème :

- créer une nouvelle application ;

- ajouter un TBitBtn à la fiche ;

- modifier sa propriété Kind pour qu'elle soit sur bkClose ;

- lancer le programme…

Alors que le bouton affichait correctement « Fermer » dans l'EDI, il affiche sa version anglaise « Close » dans notre programme ! Qui plus est, ce comportement qui se répète pour tous les éléments qui ont besoin d'une traduction est indétectable lors de la réalisation de l'interface.

Une solution partielle consiste à forcer la réécriture des éléments à traduire en les modifiant dans l'inspecteur d'objets. Malheureusement, cette solution ne règle pas les messages d'erreur lors des exceptions qui apparaîtront en anglais.

En fait, afin d'obtenir une traduction française de tous les messages venant de la bibliothèque principale LCL, il est absolument nécessaire de modifier le fichier de projet. Pour cela :

- choisir dans le menu « projet », l'élément « inspecteur de projet » afin de faire apparaître la fenêtre du projet ; cliquer sur le fichier <nom_du_projet>.lpr qui contient le code source du projet ;

- repérer le chemin qui mène vers le fichier lclstrconsts.fr.po (normalement, pour Windows : <emplacement_de_Lazarus>\lcl\languages\) ;

- modifier le projet en utilisant le listing qui suit(19) !

uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, bgrabitmappack, Main, GVAbout, Help, GVTools, GVConsts, GVTurtles,
SysUtils, GetText, Translations; // traduction française de la LCL

{$R *.res}

procedure TranslateLCL;
// *** traduction ***
var
Lang, DefLang: string;
begin
Lang := EmptyStr;
Deflang := EmptyStr;
GetLanguageIDs({%H-}Lang, {%H-}DefLang);
// utilisation du fichier corrigé
TranslateUnitResourceStrings('LCLStrConsts',
'..\..\3rdparty\lclstrconsts.fr.po', Lang, DefLang);
end;

begin
RequireDerivedFormResource := True;
TranslateLCL; // traduction
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.

IV - Les outils de programmation

Cette partie décrit deux outils fondamentaux pour le fonctionnement de GVLOGO : les piles et l'évaluateur d'expressions numériques.

IV-A - Les piles

IV-A-1 - Définition

Une pile informatique peut être figurée par une pile d'assiettes posées sur une table : lorsque j'empile une donnée (push en anglais), je pose une assiette et lorsque je dépile une donnée (pull en anglais), je retire une assiette de la pile. La dernière donnée est donc la première à pouvoir être extraite : on parle de pile LIFO (Last In First Out = dernier entré, premier sorti). Une pile permet en fait de mémoriser les données manipulées dans l'ordre où elles seront à utiliser.

Les piles sont très utilisées par GVLOGO. Elles permettent tout à la fois de faire fonctionner l'interpréteur en stockant des données à récupérer plus tard dans un certain ordre, et d'effectuer des calculs complexes, en particulier pour l'évaluation d'expressions arithmétiques telles que (2 * 4,5) / 8. Comme la lecture d'une ligne de programme s'effectue par convention de la gauche vers la droite, si l'on rencontre une opération, rien n'indique a priori que les arguments nécessaires sont fournis : mentalement, nous découvrons les éléments au fur et à mesure de notre lecture. Un programme fait de même !

IV-A-2 - Exemples

Les exemples qui suivent seront très détaillés. En effet, le fonctionnement d'une pile est au cœur de celui de l'interpréteur lui-même.

IV-A-2-a - Un exemple simple

On souhaite effectuer l'opération SOMME 24 35qui additionne les deux nombres fournis en paramètres. Pour cet exemple, on utilisera deux piles, la première contenant les données, la seconde les opérations à effectuer et le nombre de paramètres attendus.

Tout d'abord, on rencontre une opération à effectuer : ici, une addition. On sait qu'elle nécessite deux données : on empile cette opération et le nombre de paramètres attendus.

<pile vide>

2

SOMME

Ensuite, on rencontre une donnée. Afin de la mémoriser, on empile 24. SOMMEn'attend plus qu'un paramètre : on décrémente le sommet de la seconde pile :

24

1

SOMME

On rencontre encore une donnée. On empile 35et on décrémente le sommet de l'autre pile :

35

0

24

SOMME

Le 0 au sommet de la seconde pile indique que les données nécessaires sont acquises. On retire le sommet de la seconde pile pour en extraire l'opération qui peut à présent être effectuée.

35

SOMME

24

On effectue alors l'addition sur la pile :

59

<pile vide>

On récupère le résultat (59), par exemple pour l'afficher, et la première pile est vide.

Cette technique permet par conséquent de mémoriser des résultats intermédiaires : on peut imaginer que 35 est le résultat d'une autre opération et sa présence sur la pile fournira toujours le second élément nécessaire à l'addition initiale.

IV-A-2-b - Un exemple plus complexe

On souhaite à présent effectuer l'opération SOMME 24 PRODUIT 5 7, sachant que PRODUITmultiplie deux données sur la pile.

Tout d'abord, on rencontre l'opération à effectuer : ici, une addition. On sait qu'elle nécessite deux données :

<pile vide>

2

SOMME

Ensuite, on rencontre une donnée. Afin de la mémoriser, on empile 24.

SOMMEn'attend plus qu'un paramètre.

On décrémente le sommet de la seconde pile :

24

1

SOMME

On rencontre une seconde opération qui est PRODUIT, elle-même nécessitant deux données. Il faut empiler ces informations :

24

2

PRODUIT

1

SOMME

La donnée suivante est 5, qu'on empile en décrémentant par ailleurs le sommet de l'autre pile :

5

1

24

PRODUIT

1

SOMME

On a donc une opération SOMMEpendante qui attend encore une donnée et une opération PRODUITqui elle aussi attend une donnée.

La donnée suivante est 7 qu'on empile :

7

0

5

PRODUIT

24

1

SOMME

Le 0au sommet de la seconde pile indique que l'opération pendante qui la suit a les paramètres qu'elle attend à sa disposition. On dépile ce 0devenu inutile.

7

PRODUIT

5

1

24

SOMME

On récupère alors PRODUIT. On peut donc effectuer cette opération et déposer son résultat sur le sommet de la première pile en décrémentant le sommet de la seconde pile.

35

0

24

SOMME

On en a terminé avec la multiplication. La pile contient les deux données nécessaires à SOMME: on peut donc l'effectuer comme dans l'exemple 1.

Le 0 au sommet de la seconde pile indique que les données nécessaires sont acquises. On retire le sommet de la seconde pile pour en extraire l'opération qui peut à présent être effectuée.

35

SOMME

24

On effectue alors l'addition sur la pile :

59

<pile vide>

On récupère le résultat (59), par exemple pour l'afficher, et la première pile est vide.

Il est bien entendu que cet enchaînement d'opérations exige des éléments non encore réalisés :

- le flux du texte à exécuter doit être acquis et analysé pour en définir les éléments de base ;

- il faut pouvoir distinguer une opération d'une donnée ;

- le nombre de paramètres d'une opération doit être retrouvé pour être empilé ;

- une pile fournit au minimum les moyens d'empiler et de dépiler une donnée. D'autres opérations élémentaires sont souvent utiles : duplication de la dernière donnée, test de la profondeur de la pile, incrémentation et décrémentation du sommet… Les piles d'entiers et de réels fournissent d'autre part l'essentiel des opérations qu'on peut effectuer sur les données numériques qu'elles gèrent : addition, soustraction…

IV-A-3 - Opérations sur les piles (et les queues)

GVLOGO n'a pas pour vocation de traiter des opérations de bas niveau comme les piles, mais il en fournit cependant une structure minimale. De plus, il implémente une structure de queue : les éléments sont alors stockés les uns après les autres, et c'est le plus ancien qui est déstocké le premier.

Les primitives ci-après sont décrites dans ce chapitre, mais relèvent du traitement des listes. Elles simulent le fonctionnement d'une pile ou d'une queue à partir d'une liste.

- EMPILE: attend le nom d'une variable puis un mot ou une liste - ne renvoie rien - la primitive place le second paramètre au début de la variable qui doit être une liste.

Exemples :

EMPILE "MAPILE 124→ -

EMPILE "MAPILE 568→ -

- SOMMET : attend le nom d'une variable en entrée - renvoie une liste ou un mot - la primitive renvoie le dernier élément stocké dans la pile indiquée par le paramètre qui doit être une liste, sans le retirer de cette dernière.

Exemple :

ECRIS SOMMET "MAPILE→ 568

- DEPILE : attend le nom d'une variable en entrée - renvoie une liste ou un mot - la primitive renvoie le dernier élément stocké dans la pile indiquée par le paramètre qui doit être une liste, en le retirant de cette dernière.

Exemples :

ECRIS DEPILE "MAPILE→ 568

ECRIS DEPILE "MAPILE→ 124

- QUEUE : attend une variable puis une liste ou un mot en entrée - ne renvoie rien - la primitive stocke le second paramètre dans la première liste pour former une queue.

Exemple :

QUEUE "MAQUEUE [ceci est stocké]→ -

- DEQUEUE : attend une variable en entrée - renvoie une liste ou un mot - la primitive renvoie le premier élément stocké dans une queue en le retirant de cette dernière.

Exemple :

ECRIS DEQUEUE "MAQUEUE→ un ancien élément stocké

Les notions de queue et de pile sont très souples pour GVLOGO si bien qu'il est possible de traiter n'importe quelle variable, pourvu qu'elle renvoie une liste. De plus, les primitives ENQUEUEet EMPILEsont strictement équivalentes : on peut donc empiler une donnée et traiter la liste comme une pile, une queue ou une liste ordinaire, et réciproquement !

IV-A-4 - Implémentation des piles

L'unité GVStacks offre une classe de pile générique et les classes nécessaires pour manipuler des entiers, des réels et des chaînes de caractères.

On aura tout intérêt, si l'on utilise Delphi, à utiliser les piles fournies dans System.Generics.Collections(20). Comme Lazarus ne sait pas encore traiter les classes génériques de manière aussi satisfaisante que Delphi, la solution adoptée pour le projet est de travailler à partir d'une classe générique commune. Elle est plus limitée dans la mesure où elle ne connaît pas d'énumération. D'autre part, la notification est moins complète, Lazarus n'acceptant que les classes comme support des génériques. En revanche, plusieurs fonctionnalités intéressantes ont été ajoutées.

IV-A-4-a - Constantes

L'unité GVConsts a été complétée. On a fixé la taille minimale de la pile à 8, ce qui permet de ne pas réallouer en permanence de la mémoire pour l'accroissement de la pile.

De nouvelles erreurs possibles ont été par ailleurs prévues dans GVErrConsts. On retrouve ces erreurs dans les chaînes de ressources :

IE_EmptyStack = 'ERREUR INTERNE - La pile interne "%s" est vide.';
IE_OutOfMemory =
'ERREUR INTERNE - La mémoire est insuffisante pour la pile "%s".';
IE_LowStack = 'ERREUR INTERNE - Pas assez d''éléments dans la pile "%s".';

On remarquera que ces erreurs sont marquées comme internes, car elles ne se produiront que si notre programme final connaît un problème de conception.

Enfin, des constantes énumérées ont été définies pour les notifications :

// *** notifications de la pile ***
// ajout, suppression, changement, effacement
TGVStackNotification = (stAdded, stRemoved, stChanged, stCleared);

La première indique qu'un élément a été ajouté à la pile, la deuxième qu'un élément a été retiré, la troisième qu'un changement autre est intervenu, la dernière que la pile a été remise à zéro.

IV-A-4-b - La classe TGVSTack

Les piles sont implémentées en utilisant les classes génériques. Pour rappel, ces classes fournissent un modèle sur lequel viendront s'appuyer des classes spécialisées. Ainsi, à partir d'une seule classe de pile, on peut créer toutes les piles imaginables simplement en précisant le type à utiliser.

Les avantages évidents de cette méthode sur celle plus classique de définitions de classes au cas par cas sont un gain de temps, une souplesse largement accrue, une sécurité renforcée puisque le code n'a pas à être ressaisi pour chacune des classes et la possibilité de modifier le comportement de toutes les classes spécialisées à partir de la seule modification de la classe générique :

// *** pile générique ***
generic TGVStack<T> = class(TObject)

Les piles spécialisées prédéfinies sont les suivantes :

// *** piles spécialisées ***
TGVIntegerStack = specialize TGVStack<Integer>;
TGVRealStack = specialize TGVStack<Real>;
TGVStringStack = specialize TGVStack<string>;
TGVDoubleStack = specialize TGVStack<Double>;
TGVExtendedStack = specialize TGVStack<Extended>;
TGVEvalStack = specialize TGVStack<TGVBaseItem>;

En ce qui concerne l'interface de la classe TGVStack, en voici le listing :

// *** pile générique ***
generic TGVStack<T> = class(TObject)
private
fItems: array of T;
fError: TGVErrors; // gestionnaire des erreurs
fCount: Integer; // nombre d'éléments
fCapacity: Integer; // capacité actuelle
fOnNotify: TGVStackEvent; // notification
procedure Expand; // expansion si nécessaire
function GetCapacity: Integer; // capacité actuelle
function GetItem(N: Integer): T;  // accès à un élément
procedure SetCapacity(const Value: Integer); // fixe la capacité
procedure SetItem(N: Integer; AValue: T);
protected
procedure Notify(Action: TGVStackNotification); virtual; // notification
procedure DoPush(const Value: T); // empilement
function DoPop: T; // dépilement
public
constructor Create; overload; // création
destructor Destroy; override; // destruction
procedure Clear; // nettoyage
function IsEmpty: Boolean; inline; // pile vide ?
procedure Push(const Value: T); // empilement avec notification
function Pop: T; // dépilement avec notification
function Peek: T; // sommet de la pile
procedure Drop; // sommet de la pile éjecté
procedure Dup; // duplication au sommet de la pile
procedure Swap; // inversion au sommet de la pile
procedure Over; // duplication de l'avant-dernier
procedure Rot; // rotation au sommet de la pile
procedure Shrink; // contraction de la pile
function Needed(Nb: Integer): Boolean; // nombre d'éléments désirés
property Count: Integer read fCount default 0; // compte des éléments
// capacité de la pile
property Capacity: Integer read GetCapacity write SetCapacity
default CMinStack;
// accès direct à un élément
property Item[N: Integer]: T read GetItem write SetItem; default;
// notification d'un changement
property OnNotify: TGVStackEvent read fOnNotify write fOnNotify;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
end;

Cette interface appelle les remarques suivantes :

- le paramètre T qui apparaît souvent sera celui remplacé par le type réel mis en œuvre par la classe spécialisée ;

- le fondement de la pile est le tableau ouvert fItemsqui verra son dernier élément pointé par le champ privé fCount;

- la notion de capacité (à travers le champ privé fCapacity) complique un peu la classe, mais permet d'accélérer les empilements en ne procédant pas sans cesse à des allocations de mémoire ;

- la propriété Itemest définie par défaut, ce qui signifie par conséquent que l'utilisateur n'a pas besoin de la spécifier  (les deux écritures sont équivalentes : MaPile.Item[2]et Mapile[2]) ;

- la plupart des méthodes définies sont destinées à manipuler la pile : Push, Peek, Pop, Drop, Dup, Swap, Overet Rot. Les programmeurs en Forth reconnaîtront là des outils très familiers ;

- la présence des méthodes DoPushet DoPopse justifie par le fait que ces opérations de base sur la pile sont employées par les autres méthodes qui ne doivent déclencher l'événement OnNotifyqu'une seule fois : ce sont donc deux méthodes qui ne notifient rien, contrairement à Pushet à Pop.

IV-A-4-c - Test de l'unité TGVStacks

Pour ne pas changer, le programme de test est décliné en deux versions :

- Lazarus pour Windows 32 ;

- Lazarus pour Linux.

La méthode StackChangedexige quelques commentaires :

procedure TMainForm.StackChanged(Sender: TObject; Act: TGVStackNotification);
// changement de la pile
var
Li: Integer;
begin
for Li := 0 to fStackStr.Count - 1 do
sgStack.Cells[0,Li] := fStackStr[Li];
case Act of
stAdded : begin
mmoActions.Lines.Add('>>> Un élément a été ajouté à la pile.');
sgStack.RowCount := fStackStr.Count + 4;
end;
stRemoved : begin
mmoActions.Lines.Add('<<< Un élément a été retiré de la pile.');
sgStack.Cells[0,fStackStr.Count] := EmptyStr;
end;
stChanged : mmoActions.Lines.Add('<<>> Le sommet de la pile a été modifié.');
stCleared: begin
mmoActions.Lines.Add('000 La pile est vide.');
for Li := 0 to sgStack.RowCount -1 do
sgStack.Cells[0,Li] := EmptyStr;
end;
end;
if not fErr then // si erreur inactive
UpdateButtons;
end;

Cette méthode a été affectée à la propriété OnNotifyde la pile. Elle remplit le composant TStringGridavec les éléments de la pile, répartit son travail suivant l'action notifiée et met à jour les boutons qui seront actifs ou non selon la profondeur de la pile. On remarquera la mise à jour permanente de la TStringGridafin de ne pas laisser de chaînes visibles après un dépilement, et l'augmentation si nécessaire du nombre de lignes disponibles, sans quoi une exception serait déclenchée pour un index hors limites.

Une autre méthode peut poser problème : il s'agit de celle appliquée lorsque l'utilisateur presse un bouton (btnPushClick). Cette méthode est partagée par tous les boutons de la fenêtre. Afin de déterminer quel bouton a été pressé, on se sert de la propriété TabOrderqui renvoie l'ordre du composant si l'utilisateur utilise la touche de tabulation.

On utilise plutôt la propriété particulière Tagqui a le mérite de ne pas dépendre de l'ordre des composants, mais qui ne vérifie pas qu'elle est unique. Avec TabOrder, on devra revoir le fichier source si l'on modifie l'ordre des composants ou si l'on en ajoute/supprime un. Ici, le choix de TabOrderne se justifie que par le parti pris pédagogique de montrer que plusieurs solutions existent quand il s'agit de résoudre un problème !

En voici la définition :

procedure TMainForm.btnPushClick(Sender: TObject);
// actions
var
LS: string;
begin
case (Sender as TBitBtn).TabOrder of
0 : begin
LS := 'Item : ' + IntToStr(fStackStr.Count);
fStackStr.Push(LS);
end;
1 : LS := 'Pop : ' + fStackStr.Pop;
2 : LS := 'Peek : ' + fStackStr.Peek;
3 : begin
LS := 'Swap';
fStackStr.Swap;
end;
4 : begin
LS := 'Dup';
fStackStr.Dup;
end;
5 : begin
LS := 'Rot';
fStackStr.Rot;
end;
6 : begin
LS := 'Over';
fStackStr.Over;
end;
7 : begin
fStackStr.Clear;
fStackStr.Shrink;
mmoActions.Clear; // on nettoie l'affichage
LS := '*** CLEAR ***';
end;
end;
lblCapacity.Caption := 'Capacité de la pile : ' +
IntToStr(fStackStr.Capacity);
lblDepth.Caption := 'Profondeur de la pile : ' +
IntToStr(fStackStr.Count);
if fStackStr.Error.OK then  // si pas d'erreur
MmoActions.Lines.Append(LS) // on affiche
else
fStackStr.Error.Clear; // erreur annulée
end;

Une chaîne LSest préparée avant d'être ajoutée au composant TMemo. Comme la taille de la pile peut être modifiée, les composants TLabelcorrespondants sont ajustés.

Il pouvait être plus judicieux de placer cette dernière mise à jour dans la méthode StackChangedqui couvre automatiquement tous les changements de la pile. Le lecteur pourra s'amuser à réaliser ce petit changement. Toutefois, il faut prendre garde de ne pas alourdir démesurément les gestionnaires d'événements qui sont susceptibles d'être appelés de manière très fréquente : on observerait alors un ralentissement général des performances du programme.

IV-B - L'évaluation d'une expression mathématique

IV-B-1 - Définition

Un évaluateur d'expressions mathématiques est un module chargé d'évaluer toute expression qui lui sera soumise, dans la limite des fonctions qu'il connaît.

Le calcul et l'évaluation d'expressions interviennent fréquemment dans les langages de programmation. La notation ordinaire du langage LOGO est la notation dite polonaise : on nomme l'opération pour la faire suivre par ses paramètres. Ainsi la somme de 4 et de 5 s'écrira : SOMME 4 5.

Tant que les calculs sont simples, cette notation ne pose pas de problème, mais elle devient vite difficile à comprendre pour qui utilise d'habitude, comme tout un chacun, la notation infixe : par exemple, SOMME 4 PRODUIT 4 5 donnera comme résultat 4 + (4 x 5), c'est-à-dire 24.

GVLOGO accepte aussi bien la notation polonaise préfixée que la notation infixe utilisée habituellement. Simplement, en cas d'utilisation de cette dernière, l'expression devra être placée entre parenthèses.

L'évaluateur que GVLOGO propose accepte :

- les expressions imbriquées comme (4 * (5,6 / (4 + 8,2)) + 7,1) sans limite particulière d'imbrication ;

- les variables comme :a, :var1, telles que définies par le programme auquel l'évaluation appartient ;

- toute une série de fonctions dont la liste est fournie ci-après ;

- l'interface avec le système de centralisation d'erreurs tel que défini dans l'unité GVErrors ;

- un système de notifications des événements concernant l'état de l'évaluateur.

Les deux systèmes (polonais et notation infixe) ne peuvent pas être utilisés simultanément au sein d'une expression avec parenthèses. On peut donc écrire SOMME 4 (7 * 8), mais pas (4 + PRODUIT 7 8). Une procédure ne peut pas non plus faire partie de l'expression.

GVLOGO utilise la virgule pour les nombres décimaux, conformément à l'usage des écoles. En revanche, comme tous les langages de programmation, il emploie * pour dénoter la multiplication afin d'éviter l'ambiguïté du signe x qui peut être pris pour la lettre.

IV-B-2 - Opérations dans l'évaluateur

Les fonctions que connaît GVLOGO dans une expression avec parenthèses sont plutôt nombreuses. Elles sont définies dans l'unité GVPrimConsts :

MF_DAbs = 'ABS'; // valeur absolue
MF_DAbs2 = 'ABSOLUE';
MF_DCos = 'COS'; // cosinus
MF_DCos2 = 'COSINUS';
MF_DSin = 'SIN'; // sinus
MF_DSin2 = 'SINUS';
MF_DTan = 'TAN'; // tangente
MF_DTan2 = 'TANGENTE'; // 190
MF_DSqrt = 'RACINE'; // racine carrée
MF_DSqrt2 = 'RAC';
MF_DTrunc = 'TRONQUE'; // nombre tronqué
MF_DRound = 'ARRONDI'; // nombre arrondi
MF_DSqr = 'AU.CARRE'; // nombre au carré
MF_DExp = 'EXP'; // exponentielle
MF_DFrac = 'FRAC'; // partie fractionnelle
MF_DInt = 'ENT'; // partie entière
MF_DInt2 = 'ENTIER';
MF_DLn = 'LN'; // log népérien // 200
MF_DLog2 = 'LOG2'; // log base 2
MF_DLog10 = 'LOG'; // log base 10
MF_DCoTan = 'COTAN'; // cotangente
MF_DCoTan2 = 'COTANGENTE';
MF_DArcCos = 'ARCCOS'; // arc cosinus
MF_DArcCos2 = 'ARCCOSINUS';
MF_DArcSin = 'ARCSIN'; // arc sinus
MF_DArcSin2 = 'ARCSINUS';
MF_DMinus = 'NEGATIF?'; // nombre négatif ?
MF_DPLus = 'POSITIF?'; // nombre positif ? // 210
MF_DNegate = 'OPPOSE'; // signe inversé
MF_DSign = 'SIGNE'; // signe
MF_DRandom = 'HASARD'; // nombre au hasard
MF_Not = 'NON'; // négation
// fonctions sans paramètres
MF_DPi = 'PI'; // PI sur la pile
MF_True = 'VRAI'; // valeur vrai
MF_False = 'FAUX'; // valeur faux
// fonctions infixes
MF_Or = 'OU'; // ou logique
MF_And = 'ET'; // et logique
MF_Mod = 'MOD'; // modulo // 220
MF_DPower = 'PUISSANCE'; // puissance

Les seules fonctions exclues sont celles qui font intervenir deux paramètres sans être infixes : MINIMUM, MAXIMUM…

IV-B-3 - Implémentation de l'évaluateur

L'implémentation d'un système d'évaluation d'expressions est un problème classique dans le cadre d'une formation en informatique, mais classique ne signifie pas facile. Si des évaluateurs sont disponibles sur Internet, ils sont souvent simplifiés : pas de variables, pas de fonctions, limitation des imbrications… Celui proposé ici tente de lever ces limitations.

IV-B-3-a - Les différentes notations

La notation infixe à laquelle nous sommes habitués n'est pas la plus simple à implémenter parce qu'elle doit faire usage de multiples parenthèses pour lever les ambiguïtés et/ou définir de manière claire les priorités des opérations. Par exemple, comment interpréter sans règles l'expression suivante : 4 + 5 * 6 ? Doit-on suivre l'ordre des opérateurs (ce qui donnera 4 + 5 = 9 ; 9 * 6 = 54) ? Dans la pratique, la plupart des langages de programmation fourniront un résultat très différent : 34 (obtenu en faisant 4 + 30 = 34). Ils auront en effet défini une priorité à la multiplication sur l'addition.

Dans le doute ou lorsqu'il veut rendre son travail parfaitement clair, un programmeur fera usage des parenthèses qui sont toujours prioritaires sur les règles de… priorité !

La notation induit alors une lecture particulière de l'arbre, de la racine aux feuilles (notation préfixée ou polonaise, celle native de LOGO) ou au contraire en partant des feuilles pour aller vers la racine (notation postfixée ou polonaise inversée comme dans Forth et certaines calculatrices), ou encore un parcours qui monte et descend dans l'arbre (notation infixe utilisée en général).

Dérivé de LISP, LOGO utilise la notation préfixée, car c'est la plus proche de l'expression naturelle des opérations à effectuer : faire la somme du produit de a et de b, et du produit de la somme de a et de b par c... Si cette « clarté » ne vous paraît pas évidente, essayez d'exprimer les mêmes calculs avec les autres notations…

Le premier intérêt des notations préfixée et postfixée est qu'elles peuvent se passer de parenthèses si le nombre de paramètres de chaque fonction est connu et fixe. Le second intérêt est qu'elles se prêtent très facilement à un traitement par piles, comme celles qui ont été définies dans le chapitre précédent !

On trouve ainsi de nombreuses implémentations d'évaluateurs qui font appel aux structures d'arbres et de piles. Parmi les plus pédagogiques, on consultera avec profit l'article de John Colibri (en français)(21).

IV-B-3-b - L'algorithme shunting-yard (gare de triage)

La documentation en français sur cet algorithme est rare alors qu'il est intéressant à plus d'un égard. Voici ce qu'en dit l'article de Wikipédia en anglais :

In computer science, the shunting-yard algorithm is a method for parsing mathematical expressions specified in infix notation. It can be used to produce output in Reverse Polish notation (RPN)
[…]

Like the evaluation of RPN, the shunting yard algorithm is stack-based(22)

Pour ceux qui ne lisent pas l'anglais, disons que cet algorithme convient tout à fait à notre objectif : fondé sur l'utilisation des piles, l'algorithme part d'une expression infixe pour aboutir à une expression postfixée inversée. Il est par ailleurs très rapide, ne faisant appel qu'une seule fois à l'empilement de chaque donnée.

L'algorithme lui-même est complexe : afin de ne pas alourdir le document présent, le renvoi à l'article de Wikipédia suffira. Il faut analyser le texte pour en extraire les unités lexicales de base, les passer à la moulinette de notre algorithme qui utilisera une pile et récupérer la valeur de retour. L'ensemble est légèrement compliqué par une gestion des erreurs.

L'algorithme exige tout d'abord de découper le texte d'entrée en unités de base. On définit par conséquent les différentes catégories possibles de ces unités :

// *** éléments d'une expression à évaluer ***
CTokensEnum = (cteInteger, cteReal, cteVar, cteFunction, cteBeginExp,
cteEndExp, ctePlus, cteMinus, cteMul, cteDiv, ctePower, cteGreater,
cteLower, cteEqual, cteNotEqual, cteGreaterOrEqual, cteLowerOrEqual, cteMod,
cteNot, cteAnd, cteOr, cteOrB, cteAndB, cteBoolean, cteUnKnown,
cteForbidden, cteNotSupported, cteUnaryMinus, cteUnaryPlus);

// *** élément de base d'une expression ***
TGVBaseItem = record
Token: string; // élément
Kind: CTokensEnum; // type d'élément
end;

Il exige par ailleurs de définir la priorité de chaque élément, ainsi que le type d'associativité qu'il utilise :

// *** priorité des éléments d'une expression ***
// nombre le plus élevé = priorité la moins élevée
// -1 : ne s'applique pas
// 0: (unaires) - +
// 1: non
// 2: * / % mod
// 3: + -
// 4: > < <= >=
// 5: = <> !=
// 6: &
// 7: |
// 8: et
// 9: ou
// 10: ^ puissance
// 11: ( )
CTokenPrecedence: array [CTokensEnum] of Integer = (-1, -1, -1, -1, 11, 11, 3,
3, 2, 2, 10, 4, 4, 5, 5, 4, 4, 2, 1, 8, 9, 7, 6, -1, -1, -1, -1, 0, 0);
// *** associativité des éléments ***
// 1 = droite - 0 = gauche  - -1 = ne s'applique pas
CTokenAssociation: array [CTokensEnum] of Integer = (-1, -1, -1, -1, -1, -1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0);

Pour le suivi de l'état de l'évaluateur, on définit une énumération des états possibles :

// *** états de l'évaluateur ***
TGVEvalState = (esWaiting, esTokenizing, esScanning, esComputing,
esNoInit, esOK);

IV-B-3-c - La classe TGVEval

L'interface de la classe TGVEvalcomprend de nombreuses méthodes en relation avec les fonctions utilisables dans une expression. On ne reproduit ici que les autres méthodes qui implémentent l'algorithme cité ou le mécanisme des états :

protected
// ajoute un élément au tableau des éléments
procedure AddItem(const AItem: string; AKind: CTokensEnum); virtual;
// ajoute un élément à la liste de scan
procedure AddScan(const AItem: TGVBaseItem); virtual;
procedure GetVar; virtual; // traitement des variables
procedure GetFunction; virtual; // traitement des fonctions
procedure GetNumber; virtual; // traitement des nombres
procedure Change; // notification de changement
procedure StateChange; // notification de changement de l'état
procedure Tokenize; // répartition en éléments
procedure DoScan; // on analyse les  éléments
procedure DoEvaluate; // on évalue
public
constructor Create; overload; // constructeur simple
// constructeur avec initialisation
constructor Create(const AText: string); overload;
destructor Destroy; override; // destructeur
procedure Clear; // nettoyage
function GetEnumerator: TGVEvalEnumerator; // énumération
procedure Scan; // étudie la chaîne entrée
function Association(AValue: CTokensEnum): Integer; // associativité
function Precedence(AValue: CTokensEnum): Integer; // priorité
property Text: string read fText write SetText; // expression à analyser
property ActualItem: string read fActualItem; // élément en cours
property Res: Double read fResult; // résultat de l'évaluation
// index de départ
property StartIndx: Integer read fStartIndx write SetStartIndx default 1;
property Indx: Integer read fIndx default -1; // index en cours dans la chaîne
property Count: Integer read GetCount; // nombre d'éléments
property ScanCount: Integer read GetScanCount; // nombre d'éléments après scan
property Item[N: Integer]: TGVBaseItem read GetItem; default; // liste des éléments
property ScanItem[N: Integer]: TGVBaseItem read GetScanItem; // liste pour évaluation
// état de l'évaluateur
property State: TGVEvalState read fEvalState write SetState default esNoInit;
// notification d'une erreur
property Error: TGVErrors read fError write fError;
// événement lié à la recherche d'une variable globale
property Kernel: TGVLogoKernel read fKernel write fKernel;
// événement lié à la recherche d'une variable locale
property LocVars: TGVLocVars read fLocVar write fLocVar;
// événement lié à un changement
property OnChange: TNotifyEvent read fOnChange write fOnChange;
// événement lié à un changement
property OnStateChange: TNotifyEvent read fOnStateChange write fOnStateChange;
end;

La méthode centrale est Scan, qui prend en charge les différentes étapes conduisant du texte de départ au résultat :

procedure TGVEval.Scan;
// *** analyse de la chaîne entrée ***
begin
if (State = esNoInit) then // erreur si rien à évaluer
begin
// [### Erreur: non initialisation ###]
Error.SetError(CIE_NoInit, CE_GVLOGO);
Exit; // on sort
end;
try
Change; // notification de changement
Tokenize; // répartition en éléments
if Error.OK then // pas d'erreur ?
DoScan; // on analyse
if Error.Ok then // toujours pas d'erreur ?
DoEvaluate; // on évalue
finally
State := esWaiting; // état d'attente
end;
end;

Ces étapes sont par conséquent :

- répartition avec la méthode Tokenize;

- analyse avec la méthode DoScan;

- évaluation avec la méthode DoEvaluate.

On interrompt le travail dès la première erreur rencontrée.

Tokenizes'occupe de balayer la chaîne de caractères d'entrée pour isoler chaque unité lexicale en l'associant à un type :

procedure TGVEval.Tokenize;
// *** répartit en éléments ***
var
LCh: Char;
begin
State := esTokenizing; // état mis à jour
WipeItems; // liste interne nettoyée
fIndx := StartIndx; // départ initialisé
// on balaie l'expression tant qu'il n'y a pas d'erreur
while Error.Ok and (fIndx <= Length(Text)) do
begin
LCh := fText[fIndx]; // caractère en cours
case LCh of
CBlank: Inc(fIndx); // on ignore les blancs
'0'..'9': GetNumber; // c'est un nombre
CColon: GetVar; // c'est une variable
CPlus: if (Count = 0) or (Item[Count].Kind = cteBeginExp) then
AddItem(CPlus, cteUnaryPlus) // plus unaire
else
AddItem(CPlus, ctePlus); // addition ou plus unaire
CMinus: if (Count = 0) or (Item[Count].Kind = cteBeginExp) then
AddItem(CMinus, cteUnaryMinus) // moins unaire
else
AddItem(CMinus, cteMinus); // soustraction ou moins unaire
CMul: AddItem(CMul, cteMul); // multiplication
CDiv: AddItem(CDiv, cteDiv); // division
CPower: AddItem(CPower, ctePower); // puissance
CGreater: GetDelimGreater; // plus grand ou >=
CLower: GetDelimLower; // plus petit ou <= ou <>
CEqual: AddItem(CEqual, cteEqual); // égal
CNot: GetDelimNot; // négation ou !=
COrB: AddItem(COrB, cteOrB); // ou logique |
CAndB: AddItem(CAndB, cteAndB); // et logique &
CBeginPar: AddItem(CBeginPar, cteBeginExp); // parenthèse ouvrante
CEndPar: AddItem(CEndPar, cteEndExp); // parenthèse fermante
'a'..'z', 'A'..'Z': GetFunction; // fonction
else
AddItem(LCh, cteUnknown); // enregistre le caractère interdit
// [### Erreur: caractère interdit ###]
Error.SetError(CE_BadChar, Text, fIndx - 1);
end;
end;
end;

Les méthodes appelées sont celles auxquelles une tâche particulière est donnée : par exemple, GetFunctionest une des plus complexes.

Une fois ce travail réalisé, DoScans'occupe d'implémenter l'algorithme de shunting-yard. À la sortie, on doit avoir une liste des ordres rangés selon la notation polonaise inversée : en fait, un tableau d'enregistrements de type TGVBaseItem.

Pour cela, la méthode s'appuie sur le travail précédent, en particulier la répartition des unités par types :

procedure TGVEval.DoScan;
// *** analyse des éléments (algorithme shunting-yard) ***
var
Li: Integer;
LBI1, LBI2: TGVBaseItem;
begin
State := esScanning; // état mis à jour
WipeScan; // nettoyage de la sortie
for Li := 1 to Count do // on balaie la liste
begin
LBI1 := Item[Li]; // élément en cours
case LBI1.Kind of // on analyse sa nature
cteReal, cteInteger, cteVar, cteBoolean: // <==== une constante
AddScan(LBI1); // élément pour la sortie
cteFunction: // <===== une fonction
fScanStack.Push(LBI1);
ctePlus, cteMinus, cteMul, cteDiv, ctePower, cteGreater, cteLower,
cteEqual, cteNotEqual, cteGreaterOrEqual, cteLowerOrEqual, cteMod,
cteNot, cteAnd, cteOr, cteOrB, cteAndB, cteUnaryMinus,
cteUnaryPlus: // <==== un opérateur
begin
while (not fScanStack.IsEmpty) // tant que la pile n'est pas vide
and (((Association(LBI1.Kind) = 0) and (Precedence(LBI1.Kind) >=
Precedence(fScanStack.Peek.Kind))) or
((Association(LBI1.Kind) = 1) and (Precedence(LBI1.Kind) <
Precedence(fScanStack.Peek.Kind)))) do
AddScan(fScanStack.Pop); // on stocke le sommet de la pile
fScanStack.Push(LBI1); // on empile le nouvel opérateur
end;
cteBeginExp: // <==== une parenthèse ouvrante
fScanStack.Push(LBI1); // parenthèse ouvrante empilée
cteEndExp: // <==== une parenthèse fermante
begin
// tant que pile non vide et sommet <> (
while (not fScanStack.IsEmpty) and
(fScanStack.Peek.Kind <> cteBeginExp) do
AddScan(fScanStack.Pop); // on stocke le sommet de la pile
// parenthèse ouvrante trouvée ?
if (not fScanStack.IsEmpty) and
(fScanStack.Peek.Kind = cteBeginExp) then
begin
fScanStack.Pop; // on retire la parenthèse
if (not fScanStack.IsEmpty) and // pile non vide ?
// et fonction au sommet ?
(fScanStack.Peek.Kind = cteFunction) then
AddScan(fScanStack.Pop); // on stocke le sommet de la pile
end
else
begin
// [### Erreur: parenthèses ###]
Error.SetError(CE_ParMismatch, Text, Li);
Break; // on quitte la boucle
end;
end;
end;
end;
if Error.OK then // pas d'erreur ?
begin
while (not fScanStack.IsEmpty) do
begin
LBI2 := fScanStack.Pop; // on récupère le sommet
if (LBI2.Kind in [cteBeginExp, cteEndExp])then
begin
// [### Erreur: parenthèses ###]
Error.SetError(CE_ParMismatch, Text);
Exit; // on quitte la procédure
end
else
AddScan(LBI2); // on stocke
end;
end;
end;

La liste obtenue est alors directement exploitable pour l'évaluation proprement dite. Cet ultime travail est effectué par DoEvaluatequi inclut une sous-procédure DoFunction:

procedure TGVEval.DoEvaluate;
// *** évaluation ***
var
Li: Integer;
LWhich: TGVBaseItem;

procedure DoFunction;
// traite une fonction
var
Lfunc: TGVFunctions;
begin
LFunc := WhichFunction(LWhich.Token); // numéro de fonction
if fDStack.Needed(1) then // il faut un élément sur la pile
begin
case LFunc of // choix de la fonction
C_DAbs, C_DAbs2: DoAbs; // valeur absolue
C_DCos, C_DCos2: DoCos; // cosinus
C_DSin, C_DSin2: DoSin; // sinus
C_DTan, C_DTan2: DoTan; // tangente
C_DSqrt, C_DSqrt2: DoSqrt; // racine carrée
C_DTrunc: DoTrunc;  // nombre tronqué
C_DRound: DoRound;  // nombre arrondi
C_DSqr: DoSqr; // nombre au carré
C_DExp: DoExp; // exponentielle
C_DFrac: DoFrac; // partie fractionnelle
C_DInt, C_DInt2: DoInt; // partie entière
C_DLn: DoLn; // log népérien
C_DLog2:  DoLog2; // log base 2
C_DLog10: DoLog10; // log base 10
C_DCoTan, C_DCoTan2: DoCoTan; // cotangente
C_DArcCos, C_DArcCos2: DoArcCos; // arc cosinus
C_DArcSin, C_DArcSin2: DoArcSin; // arc sinus
C_Minus: DoMinus;// négatif ?
C_Plus: DoPlus; // positif ?
C_DNegate: DoNegate; // signe inversé
C_DSign: DoSign; // signe
C_DRandom: DoRandom; // entier au hasard
end;
end
else
// [### Erreur: pas assez d'arguments ###]
Error.SetError(CE_NoArg, Text, Li);
end;

L'objectif de cette sous-procédure est de traiter les fonctions rencontrées en les répartissant grâce à une structure case…of.

DoEvaluaterépartit de même tous les autres éléments en balayant tout simplement le tableau réalisé lors des étapes passées :

begin
State := esComputing; // état mis à jour
fResult := 0; // résultat à zéro
for Li := 1 to ScanCount do // on balaie les valeurs
begin
if not Error.OK then
Break; // on sort en cas d'erreur
LWhich := ScanItem[Li]; // élément en cours
fActualItem := LWhich.Token; // élément en cours stocké
Change; // notification de changement
with fDStack do
case LWhich.Kind of // répartition suivant la nature de l'élément
cteReal, cteInteger, cteVar,
cteBoolean: DoNumber(LWhich.Token); // nombre empilé
ctePlus: DoAdd; // addition ?
cteMinus: DoSub; // soustraction
cteMul: DoMul; // multiplication
cteDiv: DoDiv; // divivsion
ctePower: DoPower; // puissance
cteGreater: DoGreater; // >
cteLower: DoLower; // <
cteGreaterOrEqual: DoGreaterOrEqual; // >=
cteLowerOrEqual: DoLowerOrEqual; // <=
cteEqual: DoEqual; // =
cteNotEqual: DoNotEqual; // != ou <>
cteMod: DoMod; // mod
cteNot: DoNot; // non
cteOr, cteOrB: DoOr; // ou
cteAnd, cteAndB: DoAnd; // et
cteFunction: DoFunction; // une fonction
cteUnaryMinus: DoUnaryMinus; // moins unaire
cteUnaryPlus: DoUnaryPlus; // plus unaire
end;
end;
// récupération du résultat si disponible
if Error.OK then // pas d'erreur ?
begin
if fDStack.Count = 1 then // un élément attendu pour le résultat
begin
fResult := fDStack.Pop;  // c'est celui qui est sur la pile
State := esOk; // résultat trouvé
Change; // notification de changement
end
else
// [### Erreur: des éléments restent ###]
Error.SetError(CE_BadExp, Text);
end;
end;

Le travail se fait par l'usage intensif de la pile fDStackde type TGVDoubleStack. Le résultat final, s'il existe, est récupéré au sommet de cette pile.

IV-B-3-d - Test de l'unité GVEval

Contrairement aux autres unités, l'unité GVEval est fournie avec un programme de test qui ne pourra être compilé qu'après le chapitre suivant. En effet, cette unité est logiquement placée dans la partie « outils de programmation », mais elle fait appel pour l'utilisation des variables au noyau qui sera vu un peu plus tard.

Sans surprise, le programme de test est décliné en deux versions :

- Lazarus pour Windows 32 ;

- Lazarus pour Linux.

L'intérêt qu'il présente est de rendre explicite chacune des étapes décrites ci-dessus : il affiche aussi bien la décomposition en unités lexicales que la transformation en notation polonaise inversée. Parallèlement sont affichés les différents états de l'évaluateur. Les erreurs provoquent l'interruption de l'évaluation et l'affichage de messages adaptés.

Sans surprise aussi, ce programme de test utilise les éléments déjà vus concernant la gestion des événements :

procedure TMainForm.FormCreate(Sender: TObject);
// création de la fiche
begin
Kernel := TGVLogoKernel.Create; // noyau créé
// on crée des variables
Kernel.AddVar('var1', '123');
Kernel.AddVar('var2', '45,56');
Kernel.AddVar('var3', 'plouf');
Compute := TGVEval.Create; // objet créé
Compute.Kernel := Kernel;
Compute.Error.OnError := @GetError; // gestionnaire d'erreurs
Compute.OnChange := @GetChange; // gestionnaire de changement
Compute.OnStateChange := @GetStateChange; // gestionnaire de changement d'état
end;

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 cotesont 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 choixn'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 TGVLogoKernelest 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]]

pasest 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 .VARnomme 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 Loadou Savesont bien plus longues, leur fonctionnement reste fondé sur le même principe.

Ajouter un objet à l'espace de travail fWorkZoneest 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 TLabelpar exemple.

V-B-2 - Les variables globales

V-B-2-a - Définition

Les variables globales sont des variables dont la visibilité est sans limite 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 source de confusion possible.

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, CHOSErenvoie 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 TGVLogoKernelqui 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 ce nom, si nécessaire et sur la même ligne, 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.PROCSsupprime 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 GVKernelchargées de manipuler les procédures.

Voici la partie interface de TGVKernelconcernant 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;
// envoi 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 TStringscomme EditToProcou 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 PAQUETcré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 carrepossède alors deux propriétés : .PRC qui indique qu'il s'agit d'une procédure et .INPqui 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é .BURest 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 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é .BURde 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'erreur 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 les objectifs. Leur implémentation ne pose pas de problèmes autres que ceux étudiés jusque-là.

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

- ECRIScommence 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 ECRISsur 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.

- SOMMEcommence par une lettre.

- C'est donc une primitive.

- On cherche combien de paramètres elle attend.

- La réponse est 2.

- On empile SOMMEsur 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.

- 4commence par un chiffre.

- C'est donc un nombre.

- On empile 4sur 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.

- 5commence par un chiffre.

- C'est donc un nombre.

- On empile 5sur 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 on récupère SOMME.

Q1

PD

PC

PP

PE

ECRIS

1

5

4

- On exécute la méthode qui traite la commande SOMMEavec les paramètres 4 et 5qui 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 ECRISavec le paramètre 9.

- La méthode ECRISaffiche 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 TGVAutomatqui constitue le cœur de l'interpréteur. Au cœur même de cette classe, on trouve la méthode Processqui 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 Processet 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;

TGVAutomatRecest un enregistrement qui rassemble les éléments utiles à la description de l'état de l'interpréteur à un instant donné. fLevelcorrespond 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 CStatesArraycontient 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 commande 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 DoBeginest de préparer l'interpréteur en empilant en ordre inverse sur fWkStackles é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 les indicateurs utiles à certaines primitives (SINON, SI.VRAI, SI.FAUX) et ceux de fWkRecde 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 Processest 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'une 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 DoEnds'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 DoEndsignalera 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

DoWordse 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, DoListindique 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, DoVarcherche 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é
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

DoNumbervé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

DoEvalest elle aussi une méthode très simple : elle envoie à PushConstle 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é fReturnFlagest l'indicateur dont il faut tenir compte dans 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 DoCommandest 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

DoPrims'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ée
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

DoProcest similaire à DoPrimen 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

ExeCommandfonctionne à la manière de DoCommanden tant que simple aiguillage capable de distinguer les primitives des procédures. C'est le caractère $ introduit en préfixe par DoPrimqui 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 ExePrimest 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 fParamsStackle 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 DoExePrimqui 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

DoExePrimest 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, ExeProcest 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, ExeProcdoit 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 DoExePrimet 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 TGVAutomatMessagetraite 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 Cmdest du type TGVAutomatCmd, une énumérationdont 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 SetMessageAllqui 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é Messagede 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 acWriteconduit à l'écriture dans un composant de type TMemoalors que la commande acReadListrenvoie 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, les codes 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 HASARDest 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 commande écrite dans le composant TEditen haut à droite de l'écran.

L'utilisation des boutons « Trace » et « Trace profonde » permet 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.

VII - Le programme final

Cette partie décrit de manière succincte le logiciel final GVLOGO. Il ne s'agit plus de tests d'unités, mais de la mise en œuvre de tout le travail précédent.

VII-A - Aperçu de l'organisation du logiciel

On reconnaît les différentes unités créées dans les chapitres précédents : elles sont toutes contenues dans le répertoire units.

Les nouvelles unités sont presque toutes des fiches préfixées par frm. Leur nom devrait éclairer sur leur rôle qui est rappelé par les commentaires au sein du code lui-même.

L'unité principale se nomme main.pas. L'unité GVLOGOConsts contient les constantes propres au programme final.

Une unité originale étudiée plus loin gère la coloration syntaxique de l'éditeur : il s'agit de GVHighlighter. L'unité frmeditor est la plus intéressante puisqu'elle contient à la fois les méthodes pour la coloration syntaxique et celles pour l'interprétation du contenu de l'éditeur.

Les dépendances nouvelles sont Printer4Lazarus, CodeTools, SynEdit et richmemopackage. Seul ce dernier paquet n'est pas fourni avec Lazarus : il figure dans le dossier « 3rdparty » avec BGRABitmap et doit être installé pour faire fonctionner l'ensemble.

VII-A-1 - La coloration syntaxique

Pour son propre fonctionnement, Lazarus utilise une version du paquet SynEdit qui permet entre autres la coloration syntaxique d'un éditeur. Comme ce paquet est utilisé de manière interne, il est inutile de l'installer. Il suffit de l'inclure dans les paquets requis pour qu'il soit accessible.

Livré avec des unités de coloration pour de nombreux langages, il fournit aussi des outils pour mettre au point un éditeur adapté aux besoins d'un projet particulier. C'est pourquoi il en est fait usage dans GVLOGO.

Afin de faciliter la compréhension des problèmes à résoudre et des solutions apportées, un dossier spécifique contient le test de l'unité GVHighlighter. Son développement s'appuie pour une large part sur le tutoriel fourni dans le sous-dossier SynEdit du dossier « examples » de Lazarus.

Le travail de coloration s'appuie sur les mêmes critères que l'interpréteur, à savoir le premier caractère de chaque unité lexicale. La difficulté supplémentaire ne concerne que les commentaires à repérer puisqu'ils conditionnent toute la ligne et non une unité particulière.

C'est la méthode Nextqui s'occupe de ces difficultés :

procedure TGVHighlighter.Next;
// *** caractère suivant ***
var
Li: Integer;
LB: Boolean;
begin
fTokenPos := fTokenEnd; // position en cours
Li := Length(FLineText); // longueur de la ligne en cours
if fTokenPos > Li then // fin de ligne ?
Exit // on sort
else
// une chaîne ?
if fLineText[fTokenEnd] = CQuote then
begin
LB := True; // drapeau de caractère d'échappement
while (fTokenEnd <= Li) and (not (fLineText[fTokenEnd]
in [CTab, CBlank]) or LB) do
begin
LB := fLineText[fTokenEnd] = '$';
Inc(fTokenEnd); // on compte jusqu'à la fin de l'élément
end;
end
else
// un symbole ?
if fLineText[fTokenEnd] in CBraces then
Inc(fTokenEnd)
else
// un commentaire ? (seulement si commence la ligne)
if (Length(TrimLeft(fLineText)) > 1) and (TrimLeft(fLineText)[1] = CSlash)
and (TrimLeft(fLineText)[2] = CSlash) then
fTokenEnd := Length(fLineText) + 1
else
// un espace ?
if fLineText[fTokenEnd] in CBlanks then
while (fTokenEnd <= Li) and (fLineText[fTokenEnd] in CNonPrintable) do
Inc(fTokenEnd) // on saute les espaces
else
// autres cas
while (fTokenEnd <= Li) and not(fLineText[fTokenEnd]
in CSeparators) do
Inc(fTokenEnd); // on compte jusqu'à la fin de l'élément
// POUR ou FIN ? => pliage possible
if SameText(TrimLeft(Copy(fLineText, 1, fTokenEnd - 1)), P_To) then
StartCodeFoldBlock(nil)
else
if SameText(TrimLeft(Copy(fLineText, 1, fTokenEnd - 1)), P_End) then
EndCodeFoldBlock;
end;

Le programme de test se présente sous cette forme :

Un simple clic sur le bouton « Colorer » activera la coloration syntaxique :

On pourra de même plier et déplier le code des procédures.

VII-A-2 - L'interface utilisateur

Sans vouloir se substituer à un véritable mode d'emploi, une présentation rapide du logiciel final est sans doute nécessaire.

Pour l'installation, il suffit de partir du dossier fourni en téléchargement qui contient au minimum le programme GVLOGO, le fichier GVPrimDefs.txt et le dossier « samples » des exemples. Le dossier global est à placer dans l'emplacement désiré.

Les habitués de Windows savent que depuis Windows 7, il n'est plus accepté de modifications des fichiers placés dans les zones protégées qui abritent les exécutables. GVLOGO crée les dossiers adéquats dans lesquels il est conseillé de sauvegarder son travail. Dans l'attente d'un véritable programme d'installation, il est souhaitable de transférer les fichiers d'exemples dans ce dossier créé et sélectionné par défaut.

Généralement, il se présente comme suit :

En cliquant sur l'exécutable, on obtient l'écran suivant :

La fenêtre ouverte permet d'accéder à toutes les fonctionnalités du logiciel dès lors qu'elles sont opérationnelles, ce qui dépend du contexte. Le composant non visuel TActionListest le fondement de tout ce travail d'activation et de désactivation des fonctionnalités.

Afin de démarrer, il est conseillé d'ouvrir un des fichiers d'exemples comme Tout.GVL:

Le fichier est chargé en utilisant la commande « Ouvrir… » du menu « Fichier ». Dès qu'il a été chargé, l'éditeur s'ouvre. L'utilisateur a alors accès à toutes les procédures préenregistrées du projet.

Il suffit de choisir dans le menu « Exécuter », la fonction « Interpréter » pour lancer l'enregistrement des procédures présentes dans l'éditeur, mais aussi l'exécution des commandes du même éditeur.

L'observation de l'éditeur montre que des lignes de commandes seront ainsi exécutées en cas d'interprétation :

Juste après la définition d'un triangle, on a en effet une série de commandes qui demandent d'effacer l'écran, de dessiner une spirale composée d'octogones et d'écrire des messages sur l'écran de texte.

Dès que GVLOGO repère qu'il faut agir sur un des écrans (tortue ou texte), les deux apparaissent en avant-plan. On obtient donc :

La ligne de commandes affichée au-dessus des deux fenêtres de sortie, permet de donner des ordres à l'interpréteur sans intervenir dans l'éditeur. Par exemple, après ce premier dessin, il est suggéré de taper sur cette ligne les commandes suivantes :

VIDE.ECRAN TETRAEDRE 100 4

Un simple appui sur la touche « Entrée » lancera l'exécution de la procédure enregistrée, après effacement de l'écran de la tortue :

En remplaçant TETRAEDREpar NIMPORTEQUOI, on déclenche une erreur :

On pourra ainsi tester les différentes options et fonctionnalités du logiciel : les plus intéressantes sont certainement celles de débogage qui affichent en temps réel l'état des variables locales et globales, ou encore les paquets. L'activation de la trace depuis le menu « Exécuter » sera l'occasion de consulter la fenêtre de suivi. De même, une procédure est modifiable depuis le menu « Voir » dans sa partie « Débogage ». Dans le menu « Voir », l'état de la tortue et celui de l'écran de texte peuvent être modifiés directement. Enfin, les amateurs d'éditeurs analyseront avec profit le fonctionnement des menus « Éditer » et Chercher ».

VII-B - Ce qu'il reste à faire

L'objectif était de réaliser un interpréteur LOGO grâce à Lazarus. De ce point de vue, le travail est terminé ! Mais il est toujours possible d'aller plus loin…

VII-B-1 - L'urgent

Il est indispensable de rédiger un mode d'emploi de GVLOGO, ainsi qu'un guide pour l'utilisation du programme (programmation et activités pédagogiques).

VII-B-2 - Le possible et le souhaitable

Les éléments suivants sont faciles à implémenter et peuvent faire l'objet d'exercices de prolongation de cette brochure :

- implémentation des méthodes de la tortue dessinant des formes prédéfinies (comme prévu dans le présent document au chapitre sur la tortue) ;

- implémentation de méthodes complémentaires pour le texte sur l'écran de la tortue ;

- ajout de primitives concernant les dates et les heures ;

- implémentation des queues et des piles (comme prévu par le présent document au chapitre sur les piles) ;

- gestion élémentaire de la souris.

Des améliorations de détails sont sans doute souhaitables :

- utilisation de VRAIet FAUXau lieu de 0 et -1 pour les retours et les paramètres de type booléen ;

- HASARDdevrait différencier HASARD "123et HASARD 123, le premier renvoyant un élément du mot (1, 2, ou 3) et le second un nombre de 1 à 123 ; mise en place d'un système de bulles d'aide dans l'éditeur dès qu'une primitive est sous le curseur ou le pointeur de la souris ;

- un menu d'options permettant de personnaliser l'éditeur, surtout pour les couleurs employées ;

- le remplacement global dans l'éditeur avec un questionnement préalable à chaque occurrence.

Des fonctionnalités un peu plus délicates sont aussi envisageables :

- impression avec différenciation syntaxique ;

- chargement et enregistrement de fichiers de procédures sans passer par l'éditeur ;

- mise en place d'un système de points de suivi/d'arrêt lors de l'exécution d'un programme ;

- acceptation des caractères accentués dans les identificateurs ;

- les primitives ont des structures redondantes pour les nommer, donc sources d'erreurs : pourquoi ne pas tout fonder sur le fichier chargé au démarrage, ce qui permettrait par ailleurs de pouvoir modifier l'intitulé des primitives ?

La liste est loin d'être exhaustive… Alors, pour qui se dit qu'il a glané ici ou là des idées à partager avec la communauté des programmeurs en Pascal, il ne lui reste qu'à retrousser les manches et à livrer ses trouvailles !

(1)Cf. https://notepad-plus-plus.org/fr/

(2)Cf. http://geany.org/

(3)Cf. https://lazarus.developpez.com/telecharger/detail/id/433/Bibliotheque-BGRABitmap

(4)Cf. http://www.freepascal.org/docs-html/ref/refse11.html

(5)Il s'agit de la notation scientifique pour 10^2, soit 100.

(6)Le tri des caractères accentués sera correct avec Delphi, contrairement à Lazarus qui les classera après les caractères normaux.

(7)L'espace séparant les éléments d'une liste, une suite d'espaces sera par conséquent ignorée.

(8)On reviendra sur les expressions lors de l'étude de leur évaluation.

(9)Le tri des caractères accentués sera correct avec Delphi, contrairement à Lazarus qui les classera après les caractères normaux.

(10)On remarquera que les primitives concernant les piles et les queues ne sont pas implémentées dans cette unité : elles le seront dans l'unité TGVStacks.

(11)Ce caractère est celui qui figure sous le 6 du pavé alphanumérique. Il s'obtient en pressant la touche AltGr et la touche du 6. Il est choisi grâce à la propriété NameValueSeparator.

(12)En Pascal, les graphiques ont des coordonnées inversées par rapport aux habitudes en mathématiques. Ainsi le coin supérieur gauche a pour abscisse 0 et ordonnée 0. Plus l'ordonnée augmente plus le point visé se place vers le bas de l'écran.

(13)Pour les couleurs admises, se référer au tableau n°1 fourni avec FIXE.COULEUR.FOND.

(14)Le premier point est celui situé en haut à gauche tandis que le second est celui dessiné en bas à droite.

(15)Un sprite (parfois appelé lutin en français) est un objet qui se déplace sur une surface. On doit avoir l'impression qu'il glisse sur cette surface.

(16)… et deux fois plus rapide qu'un appel successif aux fonctions voulues !

(17)La détermination des cas a été établie dans une ancienne FAQ concernant Delphi dont je ne trouve plus trace… Par ailleurs, il existe une seconde version de Towards qui travaille avec une liste en entrée.

(18)CoordX et CoordY sont les coordonnées actuelles de la tortue.

(19)Dans le programme de test fourni, le fichier utilisé est sur le chemin du dossier GVLOGO. J'espère que la reproduction du fichier est fiable puisque c'est moi qui ai fourni cette traduction à l'équipe de Lazarus !

(20)Voir le dossier « EasyStack » qui contient un exemple de programme avec Delphi : on remarquera qu'en quelques lignes les extensions désirées sont implémentées.

(21)http://www.jcolibri.com/articles/colibri_utilities/interprete_d_expression/interprete_d_expression.html

(22)https://en.wikipedia.org/wiki/Shunting-yard_algorithm