codeworker -help CodeWorker manipule un type unique de structure de données, qui offre la possibilité de stocker une valeur, une référence sur une autre donnée, une liste d'éléments (ou, plus généralement, une table d'association) et une structure d'arbre. Une donnée peut représenter tout cela à la fois. Il existe une variable globale appelée project, toujours présente et accessible de tous les scripts. Cette variable est souvent utilisée pour y stocker les informations utiles lors du parsing et les transmettre à la génération de code, mais ce n'est pas une obligation car on peut à tout moment déclarer une variable locale. La particularité d'une variable locale est d'être placée sur la pile de l'application, ce qui contraint sa durée de vie au temps passé dans le bloc d'instructions où elle a été déclarée. Une fois sortie du bloc d'instructions, la variable locale est détruite.
local dessert = "tartelette";
Lorsqu'une copie complète est requise avec setall, la variable assignée est préalablement purgée :
insert dessert.pate = "sablée"; insert dessert.garniture["fraise"] = 8; insert dessert.garniture["abricot"]; // copie de la valeur seulement : "tartelette" local copieDessert = dessert; // copie de tout le graphe 'dessert', tables d'association comprises setall copieDessert = dessert; plus de valeur, plus de table d'association, plus de graphe. Dans certains cas, on peut vouloir simplement fusionner cette variable avec une autre. Dans cette opération de fusion, les attributs ou éléments de tables d'association qui n'existent pas dans la variable à fusionner, sont simplement copiés. Ceux qui n'ont pas la même valeur sont modifiés. L'opération de fusion d'une variable dans une autre s'accomplit en introduisant le mot-clé merge en tête de l'instruction d'assignation. Exemple : local dessert = "tartelette";
Il peut arriver qu'une variable doivent faire référence à une donnée, de telle sorte que lorsqu'on manipule le référant,
ce soit propagé sur le référé. C'est l'instruction ref qui se charge d'établir la référence.
Exemple :
insert dessert.pate = "sablée"; insert dessert.garniture["fraise"] = 8; insert dessert.garniture["abricot"]; local nouvelleRecette = "tarte"; insert nouvelleRecette.thermostat = "7/8"; insert nouvelleRecette.garniture["banane"] = 1; merge dessert = nouvelleRecette; // après le 'merge', la variable 'dessert' vaut: // dessert = "tarte"; // dessert.pate = "sablée"; // dessert.thermostat = "7/8"; // dessert.garniture["fraise"] = 8; // dessert.garniture["abricot"] = ""; // dessert.garniture["banane"] = 1; local especeAnimale;
La syntaxe d'un appel de fonction est similaire à celle des langages de la famille du C, à savoir un identifiant
suivi d'arguments séparés par des virgules, et placées entre parenthèses. L'identifiant est le nom de la fonction. Une
procédure est une fonction qui ne renvoie pas de paramètres.
Exemples :
insert especeAnimale["Mireille"] = "abeille"; insert especeAnimale["Camille"] = "chenille"; local insecte; ref insecte = especeAnimale["Mireille"]; // dorénavant, la variable 'insecte' pointe sur 'especeAnimale["Mireille"]'; // nous insérons un nouvel attribut pour obtenir : // 'especeAnimale["Mireille"].adresse = "sous le rosier"' // au travers du référant 'insecte' insert insecte.adresse = "sous le rosier"; // l'instruction 'localref' est un raccourci pour, en une seule fois, // déclarer une variable locale et établir la référence localref rampant = especeAnimale["Camille"]; // cliquez sur le nom des fonctions pour connaître leur signification
Le seul type de retour autorisé pour une fonction est un type de base : chaîne de caractères, entier, booléen, nombre
flottant. En aucun cas une fonction ne retournera une référence sur une variable, un graphe ou une table d'association. Si
cela s'avérait malgré tout nécessaire, le paramètre de retour devra figurer comme argument de la fonction, passé par référence
(mode by node).
Si le premier paramètre d'une fonction ou d'une procédure est une variable, il existe une écriture pseudo-appel de méthode
qui apporte parfois plus de clarté à la lecture du script :
cutString(maChaine, ';', listeResultat); prefix = leftString(listeResultat#back, 4); // affiche la date du jour au format 'dd/mm/yyyy' traceLine(formatDate(getNow(), "%d/%m/%Y")); maChaine.cutString(';', listeResultat);
CodeWorker propose un certain nombre de fonctions et procédures prédéfinies, dans des domaines aussi variés que la
manipulation des chaînes de caractères, des nombres ou des dates, la gestion de fichiers et de répertoires, la manipulation
des noeuds d'un graphe, celle des éléments d'un tableau, quelques commandes système...
prefix = listeResultat#back.leftString(4); // ici, les premiers paramètres sont des appels de fonction : // l'écriture en pseudo-méthode n'est pas permise traceLine(formatDate(getNow(), "%d/%m/%Y")); Elles sont toutes recensées par catégorie sur le site de CodeWorker à l'adresse http://www.codeworker.org/Documentation.html. Deux procédures nous serons particulièrement utiles par la suite : traceLine() et traceObject(). La première écrit une ligne de texte dans la console, tandis que la seconde décrit la structure de premier niveau d'une variable : sa valeur, ses attributs directs (avec leur valeur et les clés de leur table d'association éventuelle) et sa table d'association. Ces deux procédures sont très utiles à des fins de trace ou d'exploration du contenu d'une variable. Pour faire bonne mesure, introduisons la procédure saveProject(). Elle se charge de sauver au format XML la structure complète de la variable project, dont nous avons déjà parlé plus haut. Si vous avez pris soin d'alimenter cette variable avec toute l'information utile, le fichier XML sera son reflet exact, et facilitera par là-même la mise au point de vos scripts. Le programmeur peut implanter ses propres fonctions. L'implantation d'une fonction est annoncée par le mot-cle function. Les paramètres sont passés soit par valeur (mode par défaut), soit par référence. Le corps de la fonction est placé entre accolades. Une fonction ne peut retourner qu'une valeur, à l'exclusion de tout autre type (graphe, table d'association, référence sur une variable). Pour ce faire, on se sert du mot-clé return, éventuellement suivi d'une expression. Une fonction qui ne retourne pas de valeur rend toujours une chaîne vide. Exemple : // passage par référence: 'node'
Par défaut, les expressions s'appliquent aux chaînes de caractères, et non pas aux nombres. Les constantes numériques sont
traduites en écriture base 10 sous la forme d'une séquence de caractères, donc toutes les valeurs sont manipulées comme
des chaînes de caractères. L'opérateur + sert par exemple à concaténer les chaînes et non pas à
sommer des nombres.
Les opérateurs de comparaison travaillent eux-aussi sur les chaînes. Ainsi, (2 < 10) échoue car, en
réalité, c'est la comparaison ("2" < "10") des chaînes de caractère
correspondantes qui est réalisée.
Dans un contexte d'analyse syntaxique et de génération de code, il est rarissime d'avoir besoin de calculer sur des
nombres, puisque soit l'un, soit l'autre, ont pour tâche de manipuler du texte. C'est pourquoi ce n'est absolument pas
vécu comme une contrainte d'exécuter les expressions sur les chaînes. Cependant, pour les cas où il reste indispensable de
résoudre des expressions sur les nombres, il existe un moyen pour basculer en mode "résolution d'expressions
arithmétiques" : il suffit de placer l'expression entre symboles '$'. Ainsi, la comparaison
$2 < 10$ devient vraie, et $2 + 10$ donne 12 et non pas "210".
Comme la plupart des langages, CodeWorker admet un certain nombre de structures de contrôle. Nous avons eu l'occasion
de croiser l'instruction if au sein d'un précédent exemple. Il existe aussi le
while et le do... while. Tous ressemblent
beaucoup à leurs cousins C, C++ et Java, à la différence près qu'il n'est pas imposé de mettre les conditions entre
parenthèses.
Il existe encore d'autres structures de contrôle, mais seuls le foreach et le
switch seront abordés dans la suite de cette section.
L'instruction foreach sert à itérer les éléments d'une table d'association. Un index pointe
sur l'élément courant de la table d'association. Il y a trois fonctions qui ne peuvent s'appliquer que sur les indices de
boucle.
// passage par valeur : 'value' function salleMachine(bateau : node, commande : value) { if commande == "avant toute" { bateau.vitesse = "15 noeuds"; return true; } return false; } local yacht; if !salleMachine(yacht, "avant toute") error("Le mécanicien a rejeté l'ordre"); traceLine("vitesse = " + yacht.vitesse); La première, key(), permet d'obtenir la clé associée à l'élément courant de la table d'association. La seconde, first(), retourne vrai si l'index pointe sur le premier élément parcouru de la table. La troisième, last(), retourne vrai si l'index pointe sur le dernier élément parcouru de la table. Exemple : local especeAnimale;
donne à la console :
insert especeAnimale["Mireille"] = "abeille"; insert especeAnimale["Camille"] = "chenille"; foreach i in especeAnimale { if first(i) { traceLine("premier élément = '" + i.key() + "'"); } else if last(i) { traceLine("dernier élément = '" + i.key() + "'"); } traceLine("clé = '" + key(i) + "' valeur = '" + i + "'"); } premier élément = 'Mireille'
L'instruction switch a ceci de particulier qu'elle aiguille sur des chaînes de caractères,
contrairement à sa congénère C, C++ ou Java, qui aiguille sur des numéraires. Pour ceux qui ne sont pas familiers avec
comportement de cette commande, rappelons qu'elle permet de sauter directement à un certain bloc d'instructions (parmi
plusieurs proposés), conditionnellement à la valeur prise par une expression donnée. Pour le reste, vous en apprendrez plus
par l'exemple.
Exemple :
clé = 'Mireille' valeur = 'abeille' dernier élément = 'Camille' clé = 'Camille' valeur = 'chenille' switch(maValeur) {
Si l'on omet le default et qu'une valeur n'est reconnue par aucun des cas du switch, une erreur sera
remontée à l'exécution.
case "salade": case "radis": traceLine("légume"); // sortir du 'switch' à présent break; case "tomate": traceText("considéré comme légume, mais c'est un "); // on continue en séquence case "orange": case "banane": traceLine("fruit"); break; // ce cas prend toutes les valeurs qui commencent par 'pomme' start "pomme": traceLine("pomme ou pomme de terre"); break; default: // aucun des cas précédents n'a reconnu 'maValeur', // traitement par défaut traceLine("'" + maValeur + "' non reconnu!"); } On aura remarqué une originalité : start est un cas qui prend toutes les valeurs commençant par une portion de texte bien définie. Pour l'accomplissement des tâches d'analyse syntaxique, CodeWorker s'inspire de la notation utilisée pour décrire les grammaires formelles, appelée Backus-Naur Form, ou BNF pour les initiés. Dans l'outil, cette notation a été bien étendue pour rendre l'écriture des scripts de parsing la plus efficace possible. Partons d'un exemple de script pour découvrir quelques concepts : entier ::= ['-']? ['0'..'9']+;
Nous avons décrit là trois règles de production.
flottant ::= entier '.' entier ['e' entier]?; nombre ::= flottant | entier; La première définit ce qu'est un entier : une séquence de chiffres décimaux, éventuellement précédée d'un signe négatif. La deuxième précise comment écrire un nombre flottant : parties entière et fractionnaire séparées par un point (notation anglo-saxonne), et éventuellement un exposant. Ici, pour ne pas alourdir la grammaire mais surtout afin d'introduire plus bas un opérateur spécial, nous restons laxistes et nous autorisons temporairement la partie fractionnaire à être négative. La troisième prétend qu'un nombre est soit un flottant, soit un entier. A présent, un peu de terminologie : les règles de production sont composées d'une séquence de symboles BNF (entier '.' entier ['e' entier]? par exemple), ou d'alternatives parmi des séquences de symboles BNF (flottant | entier offre deux alternatives : soit la séquence BNF entier, soit la séquence BNF flottant). Un symbole BNF est dit terminal s'il se limite à une constante ('-' et 'e') ou à un intervalle de constantes ('0'..'9'). Un symbole BNF est dit non-terminal s'il conduit à appeler une autre règle de production (dans la séquence entier '.' entier ['e' entier]?, entier est un symbole non-terminal). Pour les puristes, par abus de langage mais pour simplifier le discours, on considère aussi la répétition d'une séquence BNF (ou d'alternatives sur séquences BNF) comme un symbole BNF. Nous avons ci-dessus l'illustration de deux types de répétition. On peut recenser cinq formes communes :
Une grammaire sensée lire un nombre commencera par décrire ce qu'est un nombre. Retouchons notre exemple initial pour que la règle de production d'un nombre se retrouve en tête de grammaire : nombre ::= flottant | entier;
Le reste de cette section vous paraîtra peut-être un peu ardu. Si c'est le cas, n'insistez pas et sautez à la prochaine
section. Vous y reviendrez plus facilement après avoir vu fonctionner les scripts de parsing.
Les règles de production parcourent un texte de la gauche vers la droite, et leurs symboles BNF s'appliquent le plus à
gauche possible (on tente de satisfaire d'abord le premier symbole de la séquence, puis on passe au suivant). Lorsqu'une
séquence BNF échoue, on essaie la prochaine alternative. S'il ne reste plus d'alternatives, on quitte la règle de
production en échec, ce qui fait échouer le symbole non-terminal qui l'a appelé. A son tour, le symbole non-terminal
provoque l'échec de la séquence BNF à laquelle il appartient, et on continue ainsi.
A procéder de la sorte, ce type de parseur est dit à descente récursive et même, plus précisément, LL(k). Le
premier L est là pour rappeler un parcours de la gauche (Left) vers la droite, le second pour signifier qu'on
résoud la règle le plus à gauche possible (Left-most), et le petit k pour dire qu'il peut anticiper plusieurs
coups d'avance (nombre indéterminé k) avant de résoudre une ambiguïté.
CodeWorker ne résoud pas automatiquement une ambiguïté; il faut parfois arranger les règles de production. Pour illustrer
ce que l'on entend pas là, reprenons l'exemple en tête de section. Un nombre est soit un flottant, soit un entier. Or,
un nombre flottant commence toujours par un entier dans notre définition. Donc, si l'on inverse maintenant la règle de
production :
entier ::= ['-']? ['0'..'9']+; flottant ::= entier '.' entier ['e' entier]?; l'on va rechercher d'abord un entier, et face à un flottant dans le texte parcouru, on ne lira que sa partie entière puis on sortira en succès du non-terminal nombre. Conséquence : le non-terminal nombre n'a accompli qu'une partie de sa tâche; il n'a pas récupéré la partie fractionnaire du flottant, et le parsing capotera plus loin, sortant en échec. Solution : ici, il faut inverser les deux alternatives (donc prendre nombre ::= flottant | entier;). Ainsi, face à un flottant dans le texte, la première séquence BNF sort en succès tandis que, face à un entier dans le texte, la recherche du flottant échoue et mène à la résolution de l'alternative, qui réussit dans la lecture d'un entier. Nous avons déjà rencontré une extension de la notation Backus-Naur; il s'agit de la répétition (['0'..'9']+ par exemple) et du caractère facultatif d'une séquence de symboles BNF ((['-']? par exemple)). Ils ne sont pas indispensables et peuvent s'exprimer uniquement à l'aide de symboles terminaux et non-terminaux, mariés à l'alternative. En revanche, pour une exploitation concrète efficace, il serait laborieux de ne pas disposer de ces raccourcis pour désigner répétition et optionnalité. Nous allons découvrir d'autres extensions de la BNF, en prenant appui sur l'exemple des sections précédentes que nous enrichissons de quelques règles de production. Exemple : On veut disposer d'une grammaire qui lit un fichier composé d'une suite de nombres et d'identifiants. Un identifiant commence par une lettre ou un underscore, et poursuit par une séquence de lettres, d'underscores et de chiffres. contenu_du_fichier ::= [nombre | identifiant]*;
Dans le fichier texte à lire, ces nombres et identifiants sont séparés par des caractères blancs, genre espaces,
tabulations et retour-chariots. Malheureusement, notre grammaire n'ignore pas ces caractères entre ces symboles
non-terminaux.
Le moment est venu d'introduire la directive #ignore(blanks). Cette directive stipule que, dorénavant,
avant chaque symbole de la séquence BNF à laquelle elle appartient, les caractères blancs seront ignorés automatiquement
par le moteur BNF. La règle de tête devient alors :
nombre ::= flottant | entier; entier ::= ['-']? ['0'..'9']+; flottant ::= entier '.' entier ['e' entier]?; identifiant ::= [lettre | '_'] [lettre | '0'..'9' | '_']*; lettre ::= 'a'..'z' | 'A'..'Z'; Seulement voilà! L'ignorance des caractères insignifiants se propage dans les règles de production appelées par les symboles non-terminaux nombre et identifiant, et ainsi de suite. Conséquence : on ignore aussi les espaces entre les chiffres d'un entier et les lettres d'un identifiant. Or, c'est erroné : un entier est une série de chiffres contigus, collés les uns aux autres. Il faut donc débrayer localement l'action d'ignorer les blancs au niveau de la définition d'un entier, d'un flottant et d'un identifiant. C'est le rôle de la directive #!ignore. On retouche donc les trois règles : entier ::= #!ignore ['-']? ['0'..'9']+;
A présent, corrigeons la règle de production du nombre flottant, où nous avions volontairement autorisé la partie
fractionnaire à être négative. Il existe un opérateur spécial, noté !symbole-BNF, qui échoue si
le symbole BNF sur lequel il porte est valide, et qui réussit en cas d'échec du symbole BNF. Dans les deux cas, la
règle de production ne progresse absolument pas dans sa lecture. Si l'on emploie une seconde fois cet opérateur (!!symbole-BNF),
il signifie évidemment qu'il réussit si le symbole BNF réussit, et qu'il échoue si le symbole BNF échoue, MAIS sans
jamais progresser dans la lecture. Avantage : cela permet d'accomplir une lecture anticipée (look ahead en anglais) du texte sans déplacer le curseur
de lecture.
flottant ::= #!ignore entier '.' entier ['e' entier]?; identifiant ::= #!ignore [lettre | '_'] [lettre | '0'..'9' | '_']*; A présent, employons l'opérateur ! avant de lire la partie fractionnaire, pour nous assurer que cette partie fractionnaire n'est pas négative : Nous avançons, mais nous n'avons pas encore le moyen de nous assurer que le texte se conforme à la grammaire. En effet, si la grammaire n'arrive pas à s'appliquer sur le fichier texte, l'analyse s'achève en silence. La directive #empty s'assure que la fin du texte a été atteinte. Ce symbole terminal échoue dans le cas contraire. Voici donc la première partie : nous sommes capable de détecter la fin du texte à lire, mais pas encore d'avertir l'utilisateur que tout s'est bien passé. Supposons que nous voulions tracer un message dans la console : nous avons besoin d'insérer un bout de script chargé d'écrire le message. L'insertion d'un bout de script s'annonce par l'opérateur => et est suivie par une instruction terminée par un point-virgule ou suivie par un bloc d'instructions fiché entre accolades. Lorsque le moteur de résolution BNF rencontre ce symbole, il comprend qu'il s'agit là d'une instruction qui ne le concerne pas, et demande son interprétation au moteur de script standard. Voici ce que devient alors notre règle de tête : contenu_du_fichier ::=
Si l'analyse aboutit, un message apparaîtra. Mais si elle échoue, comment savoir où cela s'est produit, à
quelle ligne, quelle colonne, pour quelle raison? Pour répondre à toutes ces questions, introduisons la directive
#continue. Placée dans une séquence de symboles BNF, elle impose que le reste de la séquence aboutisse.
#ignore(blanks) [nombre | identifiant]* #empty => traceLine("L'analyse a réussi!"); ; Si un symbole BNF venait à échouer dans la séquence, un message d'erreur serait remontée automatiquement, signalant la ligne et la colonne fautive, ainsi que le symbole BNF en faute. Exemple : Dans la règle de production d'un nombre flottant, si l'on arrive à lire un entier suivi d'un point, l'on a la certitude de se trouver face à un flottant. Le contraire rèvèlerait une erreur de syntaxe. Il en va de même avec l'exposant : la lettre 'e' annonce un exposant entier. contenu_du_fichier ::=
Ainsi, si la partie fractionnaire n'est pas composée d'un entier positif, ou si l'exposant n'est pas suivi d'un entier, une
erreur de syntaxe est produite, grace au placement que l'on a fait de la directive #continue. Il en est de même
si l'on n'arrive pas à atteindre la fin de fichier.
L'avantage que l'on peut tirer de l'usage du #continue est de deux natures symétriques. Durant la phase de mise au point,
si vous êtes sûr du fichier texte que vous utilisez pour les tests, si une erreur est remontée, cela signifie que vous
vous êtes trompé dans l'écriture de la grammaire. Durant la phase de production, si vous êtes confiant dans votre grammaire,
cela signifie que le fichier texte lu comporte une erreur de syntaxe. Dans les deux cas, la correction s'avère très rapide
en général, car la pile d'appel des non-terminaux est remontée avec le message d'erreur.
Enrichissons notre grammaire afin d'accepter les chaînes de caractères entre guillemets.
#continue #ignore(blanks) [nombre | identifiant]* #empty ; flottant ::= #!ignore entier '.' #continue !'-' entier ['e' #continue entier]?; contenu_du_fichier ::=
L'on a introduit un nouvel opérateur, qui s'écrit ~symbole-BNF. Il est valide et avance de une
position dans la lecture du fichier texte si le symbole BNF échoue. Il échoue et reste sur place si le symbole BNF réussit.
#continue #ignore(blanks) [nombre | identifiant | chaine]* #empty => traceLine("L'analyse a réussi!"); ; chaine ::= '"' [~'"']* '"'; // on ne rappelle pas les autres règles de production Dans la règle de production, employé avec la répétition multiple, il signifie que l'on avance d'un cran tant que le guillement final n'a pas été atteint. Il est très fréquent d'utiliser conjointement l'opérateur de négation, que nous venons de découvrir, avec la répétition multiple et la consommation du symbole BNF à atteindre : [~'"']* '"' dans l'exemple précédent, qui signifie d'avancer jusqu'à trouver le guillement, puis le consommer, c'est-à-dire le parcourir. On peut l'exprimer différemment : sauter juste après le premier guillement rencontré. Cette opération de saut peut s'écrire ->symbole-BNF, et est la forme condensée de [~symbole-BNF]* symbole-BNF. On peut réécrire la règle de production d'une chaine : Pour l'instant, notre analyse syntaxique s'est bornée à scanner le fichier texte, et n'a pas cherché à extraire l'information utile. Pour récupérer la partie du texte parcourue par un symbole BNF, il suffit de rajouter le nom de la variable à assigner, derrière le symbole et après les avoir séparé par un deux-points. Exemple : flottant ::=
Les variables iExposant et sChaine n'ont pas été déclarées préalablement. Elles sont donc créées automatiquement
sur la pile. iExposant a seulement la portée des crochets dans lesquels elle a été déclarée : elle n'est plus
visible une fois sorti de l'optionnalité. En revanche, sChaine a la protée de la règle de production.
Pour la lecture de l'exposant, tout va bien : on a bien la valeur de l'entier. En revanche, il y a un petit souci pour la
chaîne de catactères : l'opérateur de saut lit jusqu'au guillement terminal, que l'on retrouve donc dans la variable
sChaine, alors qu'on ne désirait que le texte inclus entre les guillements. On voudrait réclamer de ne récupérer
que le texte lu jusqu'avant la consommation du guillement terminal. C'est possible en précisant la variable,
précédée de deux-points et encadrée de parenthèses, entre la flêche et le symbole BNF sur lequel sauter.
Ce n'est pas encore parfait, car si la règle de tête récupère dans une variable le texte parcouru par le non-terminal
chaine, elle se retrouvera en présence des guillemets initiaux et finaux. Heureusement, on peut construire soi-même
la valeur que doit retourner une règle de production. Le nom de la règle doit être suivi de value après deux-points,
et la valeur doit être affectée à une variable qui a le nom de la règle.
#!ignore entier '.' #continue !'-' entier [ 'e' #continue entier:iExposant => traceLine("valeur de l'exposant = " + iExposant); ]? ; chaine ::= '"' ->'"':sChaine => traceLine(sChaine); ; // seul le coeur de la chaine sera retourné par la règle de production
Nous savons maintenant récupérer localement des fragments de texte, ceux qui nous paraissent pertinents. Il ne nous reste
plus qu'à les ranger dans une structure de données. Voyons comment procéder à partir de notre exemple, que nous allons
retravailler.
On considère nos fichiers texte comme une suite d'éléments. Un élément est soit un nombre, soit un identifiant, soit une
chaîne. On veut ranger dans une liste la séquence d'éléments que nous avons extraites.
chaine : value ::= '"' ->(:chaine)'"'; contenu_du_fichier ::=
Epluchons à présent cet exemple. Lorsqu'un script BNF s'exécute, il ne voit pas le scope des variables du script qui
l'a lancé. Notamment, il ne sait pas quelle variable renseigner avec les fagments de texte qu'il aura extraits du fichier.
#continue #ignore(blanks) #continue [ #pushItem(this) element(this#back) ]* #empty ; element(item : node) ::= nombre:dValeur => insert item.type = "nombre"; => insert item.value = dValeur; | identifiant:sId => insert item.type = "identifiant"; => insert item.value = sId; | chaine:sChaine => insert item.type = "chaine"; => insert item.value = sChaine; ; C'est pourquoi, chaque fois qu'on lance un script de parsing BNF, avec la procédure parseAsBNF(script, variable, fichier_texte), on lui passe le nom de la variable à renseigner. C'est le deuxième paramètre de la procédure. A l'intérieur du script de parsing BNF, cette variable s'appelle this dans tous les cas, et représente le contexte que l'appelant a bien voulu lui soumettre. Nous voulons renseigner la variable this avec une liste d'éléments. La directive #pushItem(liste) rajoute un élement dans la liste passée en paramètre : dans la table d'association, un nouvel élément est inséré avec un indice entier comme valeur de la clé. L'indice commence à compter à partir de zéro, et s'incrémente. Si la séquence BNF à laquelle appartient la directive échoue, l'élément est automatiquement supprimé de la liste. La règle de production element se charge de remplir ce nouvel élément de la liste. Pour ce faire, il est passé en paramètre à la règle de production, qui attend un argument passé par référence (le mode de passage par référence est node, comme pour les fonctions). Supposons à présent que nous voulions écrire une grammaire capable de lire la description de jouets et de desserts du genre :
$TMP/Developpez.com/objets.txt dessert "tartelette"
Vu l'état actuel de nos connaissances sur CodeWorker, on écrirait notre grammaire ainsi :
pate "sablee" garniture 2 "fraise" 8 "abricot" 0.5 jouet categorie "voiture" alimentation "piles" quantite 4 taille "LR6" objets ::=
Cette grammaire ne décrit pas tout à fait ce que nous voulons. En effet, elle n'est pas capable de tétecter une erreur de
syntaxe si le fichier texte ne comprend pas de blancs entre jouet et categorie, que les deux mots
soient attachés pour former jouetcategorie. Il aurait fallu écrire que l'on voulait lire un identifiant valant
valant jouet exactement. Cela se note identifiant:"jouet".
Supposons à présent que la taille des piles soit à prendre parmi LR3, LR4 et LR6, et que l'on veuille la récupérer dans une
variable. On écrit cela ainsi : chaine:{"LR3", "LR4", "LR6"}:sTaille.
#ignore(blanks) #continue [jouet | dessert]* #empty ; jouet ::= "jouet" #continue "categorie" chaine "alimentation" chaine "quantite" entier "taille" chaine ; dessert ::= "dessert" #continue chaine "pate" chaine "garniture" entier [chaine #continue nombre]+ ; // Il faut inclure ici les règles de production 'chaine', 'nombre', // 'entier' et 'flottant' La règle de production intègre maintenant ces deux ajustements : jouet ::=
On s'aperçoit alors que les attributs taille et quantite n'ont un sens que si l'alimentation du jouet se
fait par piles. Il existe la directive #check(expression), qui réussit si l'expression booléenne est
valide, et qui échoue dans le cas contraire.
identifiant:"jouet" #continue "categorie" chaine "alimentation" chaine "quantite" entier "taille" chaine:{"LR3", "LR4", "LR6"}:sTaille => traceLine("taille = " + sTaille); ; jouet ::=
Supposons à présent qu'il soit fréquent d'enrichir ce format de nouveaux attributs, et parfois même, de nouveaux
objets. Il peut devenir difficile de maintenir la grammaire à jour. Heureusement, il existe un moyen de détecter lorsque
la grammaire tombe sur un attribut qu'elle ne sait pas encore traiter. Cela se fait grâce aux règles de production
génériques instantiées, dont voici un exemple :
identifiant:"jouet" #continue "categorie" chaine "alimentation" chaine:sAlimentation [ #check(sAlimentation == "piles") #continue "quantite" entier "taille" chaine:{"LR3", "LR4", "LR6"}:sTaille => traceLine("taille = " + sTaille); ]? ; dessert ::=
Ici, la règle de production dessert_attribut connaît plusieurs implémentations, qui varient selon la valeur prise
par une chaîne de caractères placée entre < ... >. La règle de production dessert_attribut<"pate">
est une instantiation possible de la règle de production générique dessert_attribut<T>, que nous n'implémenterons pas ici.
Comment cela fonctionne-t-il ? Le non-terminal dessert_attribut<sAttribut> appelle la bonne instantiation de
cette règle, dépendant de la valeur prise par sAttribut : "pate" ou "garniture". Si la valeur
prise par sAttribut ne vaut ni l'une ni l'autre, l'implémentation générique de cette règle est appelée. Comme
nous ne l'implémenterons pas, une erreur est produite à l'exécution, qui précise qu'il n'a pas su instantier de règle de
production sur dessert_attribut<sAttribut> pour la valeur passée.
Si l'on rajoute l'attribut thermostat dans le fichier texte, l'interpréteur déclenchera un message d'erreur lorsqu'il
le rencontrera, précisant qu'il n'y a aucune forme de dessert_attribut<sAttribut> capable de s'instantier sur "thermostat".
"dessert" #continue chaine [ identifiant:sAttribut dessert_attribut<sAttribut> ]+ ; dessert_attribut<"pate"> ::= #continue chaine; dessert_attribut<"garniture"> ::= #continue entier [chaine #continue nombre]+ ; Le développeur n'a alors plus qu'à rajouter l'implémentation de la règle de production dessert_attribut<"thermostat">. Il existe quelques règles de production qui reviennent très souvent, comme la lecture d'un entier, d'un flottant, d'une chaîne de caractères, d'un identifiant ... et quelques autres encore. Pour celles-ci, CodeWorker propose des symboles non-terminaux prédéfinis, dont la règle est codée en dur. Dans le vocabulaire propre à CodeWorker, elles s'appellent des directives pseudo-terminales, car elles s'apparentent à des symboles BNF terminaux puisque leur règle de production est invisible. En voici quelques unes:
$TMP/Developpez.com/objets-scanner.cwp codeworker $TMP/Developpez.com/objets-scanner.cwp $TMP/Developpez.com/objets.txt -nologo objets ::=
On notera l'ajout de deux règles de production génériques, une sur la reconnaissance des attributs d'un jouet
et l'autre sur les attributs d'un dessert.
#ignore(blanks) #continue [#readIdentifier:sObjet #continue objet<sObjet>]* #empty ; objet<"jouet"> ::= #continue [ #readIdentifier:sAttribut jouet_attribut<sAttribut> ]+ ; jouet_attribut<"categorie"> ::= #continue #readCString; jouet_attribut<"alimentation"> ::= #continue #readCString:sAlimentation [ #check(sAlimentation == "piles") "quantite" #readInteger "taille" #readCString:{"LR3", "LR4", "LR6"}:sTaille ]? ; jouet_attribut<T> ::= #check(false); objet<"dessert"> ::= #continue #readCString [ #readIdentifier:sAttribut dessert_attribut<sAttribut> ]+ ; dessert_attribut<"pate"> ::= #continue #readCString; dessert_attribut<"garniture"> ::= #continue #readInteger [#readCString #continue #readNumeric]+ ; dessert_attribut<T> ::= #check(false); En effet, vue la syntaxe choisie pour décrire nos objets, lorsqu'on rencontre un identifiant, il y a ambiguïté entre nouvel attribut ou nouvel objet. Ainsi, lorsqu'un attribut ne se retrouve aiguillé sur aucune des règles de production instantiées pour leur traitement, il est récupéré par la règle générique qui simplement échoue. Sans cette règle, une erreur interromprait l'exécution, alors que l'identifiant annonce simplement un nouvel objet, traité plus loin par les règles objet<"dessert"> ou objet<"jouet">. Nous avons à présent toutes les informations en main pour construire le graphe de parsing à la lecture du texte. Voici ce que devient notre exemple : $TMP/Developpez.com/objets.cwp codeworker $TMP/Developpez.com/objets.cwp $TMP/Developpez.com/objets.txt -nologo | ||||||||||||||||||||||||||||