TDoG-Skin/vendor/hoa/compiler/Documentation/Fr/Index.xyl

1288 lines
62 KiB
Plaintext
Raw Normal View History

2024-08-17 18:43:48 +08:00
<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns="http://hoa-project.net/xyl/xylophone">
<yield id="chapter">
<p>Les <strong>compilateurs</strong> permettent d'<strong>analyser</strong> et
<strong>manipuler</strong> des données <strong>textuelles</strong>. Leurs
applications sont très nombreuses. <code>Hoa\Compiler</code> propose de
manipuler plusieurs compilateurs selon les besoins.</p>
<h2 id="Table_of_contents">Table des matières</h2>
<tableofcontents id="main-toc" />
<h2 id="Introduction" for="main-toc">Introduction</h2>
<blockquote cite="https://fr.wikipedia.org/wiki/Nicolas_Boileau">Ce qui se
conçoit bien s'énonce clairement, et les mots pour le dire viennent
aisément.</blockquote>
<p>Un <strong>langage</strong> est une façon d'exprimer ou de
<strong>formuler</strong> une <strong>solution</strong> à un
<strong>problème</strong>. Et des problèmes, il en existe beaucoup. Nous
lisons et écrivons dans plusieurs langages au quotidien, et certains de ces
langages sont <strong>compris</strong> par des <strong>machines</strong>.
Cette opération est possible grâce aux <strong>compilateurs</strong>.</p>
<p>La <a href="https://fr.wikipedia.org/wiki/Théorie_des_langages">théorie des
langages</a> étudie entre autres l'<strong>analyse automatique</strong> de ces
langages à travers des outils comme des <strong>automates</strong> ou des
<strong>grammaires</strong>. Il est nécessaire d'avoir un cours détaillé pour
bien comprendre tous ces concepts. Toutefois, nous allons essayer de
vulgariser un minimum pour permettre une utilisation correcte de
<code>Hoa\Compiler</code>.</p>
<h3 id="Language_and_grammar" for="main-toc">Langage et grammaire</h3>
<p>Un <strong>langage</strong> est un ensemble de <strong>mots</strong>.
Chaque mot est une <strong>séquence</strong> de <strong>symboles</strong>
appartenant à un <strong>alphabet</strong>. Un symbole représente la plus
petite <strong>unité lexicale</strong> d'un langage, il est atomique et nous
l'appellons <strong>lexème</strong> (ou <em lang="en">token</em> en anglais).
Les séquences de lexèmes représentant les mots sont construites avec des
<strong>règles</strong>. À partir d'un mot et d'une règle racine, nous allons
essayer de <strong>dériver</strong> ses sous-règles. Si une dérivation existe,
alors le mot est considéré comme <strong>valide</strong>, sinon il est
considéré comme <strong>invalide</strong>. Nous parlons aussi de
<strong>reconnaissance</strong> de mots. Par exemple, si nous considérons les
règles suivantes :</p>
<pre><code>  exp ::= exp + exp
  | nombre
 nombre ::= chiffre nombre
  | chiffre
chiffre ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9</code></pre>
<p>Le mot que nous voulons reconnaître est <code>7 + 35</code>. La règle
racine est <code><em>exp</em></code>. Si nous la dérivons (de gauche à droite
et de haut en bas, ou <em lang="en">left-to-right</em> et
<em lang="en">top-to-bottom</em> en anglais), nous pouvons avoir
<code><em>exp</em> + <em>exp</em></code> ou <code><em>nombre</em></code> (la
<strong>disjonction</strong>, <em>i.e.</em> le « ou », est représentée par le
symbole « <code>|</code> ») :</p>
<pre><code>exp + exp | nombre
→ exp + exp
→ ( exp + exp | nombre ) + exp
→ nombre + exp
→ ( chiffre nombre | chiffre ) + exp</code></pre>
<p>Nous continuons à dériver jusqu'à <strong>éliminer</strong> toutes les
règles et n'avoir que des <strong>lexèmes</strong> :</p>
<pre><code>…
→ ( chiffre nombre | chiffre ) + exp
→ chiffre + exp
→ ( 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ) + exp
→ 7 + exp
→ 7 + ( exp + exp | nombre )
→ 7 + nombre
→ 7 + ( chiffre nombre | chiffre )
→ 7 + chiffre nombre
→ 7 + ( 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ) nombre
→ 7 + 3 nombre
→ 7 + 3 ( chiffre nombre | chiffre )
→ 7 + 3 chiffre
→ 7 + 3 ( 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 )
→ 7 + 3 5</code></pre>
<p>Une dériviation existe bel et bien pour reconnaître le mot <code>7 +
35</code>, c'est donc un mot valide pour ces règles.</p>
<p>Un ensemble de règles est appelé une <strong>grammaire</strong>. Et donc,
une grammaire représente un <strong>langage</strong> !</p>
<p>Toutefois, il existe plusieurs catégories de grammaires. C'est en 1956 qu'a
été formulée la
<a href="https://fr.wikipedia.org/wiki/Hi%C3%A9rarchie_de_Chomsky">hiérarchie
de Chomsky</a>, classant les grammaires en quatre
<strong>niveaux</strong> :</p>
<ol>
<li>grammaires <strong>générales</strong>, ou <em lang="en">unrestricted
grammars</em>, reconnaissant les langages dits de Turing, aucune
restriction n'est imposée aux règles ;</li>
<li>grammaires <strong>contextuelles</strong>, ou
<em lang="en">context-sensitive grammars</em>, reconnaissant les langages
contextuels ;</li>
<li>grammaires <strong>algébriques</strong>, ou <em lang="en">context-free
grammars</em>, reconnaissant les langages algébriques, basés sur les
automates à pile ;</li>
<li>grammaires <strong>régulières</strong>, ou <em lang="en">regular
grammars</em>, reconnaissant les langages réguliers.</li>
</ol>
<p>Chaque niveau reconnait le niveau suivant. <code>Hoa\Compiler</code> ne
traite que les langages définis par les grammaires de niveau 3 et 4. Pour
donner rapidement une idée, les grammaires régulières peuvent s'apparenter aux
<a href="https://fr.wikipedia.org/wiki/Expression_régulière">expressions
régulières</a> (comme les <a href="http://pcre.org/">PCRE</a>), bien connues
des développeurs. Mais les grammaires régulières ne permettent pas par exemple
de reconnaître des <strong>couples de symboles</strong> (comme des
parenthèses, des accolades ou des guillemets), alors que les grammaires
algébriques le permettent (grâce à la notion de piles de lexèmes).</p>
<h3 id="Matching_words" for="main-toc">Reconnaissance de mots</h3>
<div id="parsers" class="schema"></div>
<script>
Hoa.Document.onReady(function ( ) {
var paper = Hoa.Graph(Hoa.$('#parsers'), 800, 180);
var grid = paper.grid(0, 0, 800, 180, 5, 2);
var word = grid.push(paper.rect(0, 0, 140, 80, 3, 'mot'), 0, 0);
var sequence = grid.push(paper.rect(0, 0, 140, 80, 3, 'séquence'), 2, 0);
var trace = grid.push(paper.rect(0, 0, 140, 80, 3, 'résultat'), 4, 0);
grid.push(paper.rect(0, 0, 140, 50, 3, 'abcdef'), 0, 1);
grid.push(paper.rect(0, 0, 380, 50, 3, '[[a ⟼ …], [bc ⟼ …], [d ⟼ …], [ef ⟼ …]]'), 2, 1);
grid.push(paper.rect(0, 0, 140, 50, 3, 'valide/invalide'), 4, 1);
paper.link.between(word, sequence, 'analyseur lexical');
paper.link.between(sequence, trace, 'analyseur syntaxique');
});
</script>
<p>En général, le processus de compilation débute par deux
<strong>analyses</strong> : <strong>lexicale</strong> et
<strong>syntaxique</strong>. Une analyse lexicale consiste à
<strong>découper</strong> un mot en une <strong>séquence de lexèmes</strong>.
Cette séquence sera ensuite utilisée par l'analyseur syntaxique afin de
vérifier que le mot <strong>appartient</strong> au langage.</p>
<p>Selon la grammaire, la reconnaissance ne se fera pas de la même manière,
mais le principe reste identique : prendre les lexèmes les uns après les
autres dans la séquence et vérifier qu'ils permettent
d'<strong>avancer</strong> dans la <strong>dérivation</strong> des règles de
notre grammaire.</p>
<p>Les analyses syntaxiques sont aussi classées en
<strong>catégories</strong> : LL, LR, LALR etc. <code>Hoa\Compiler</code> ne
propose que des analyseurs syntaxiques LL, pour <em lang="en">Left-to-right
Leftmost derivation</em>, <em>i.e.</em> de la plus haute règle vers la plus
profonde, et les règles sont dérivées de la gauche vers la droite. Là encore,
il existe des sous-catégories, dont deux que traite
<code>Hoa\Compiler</code> : LL(1) et LL(*). D'une manière générale, nous
parlons d'analyseurs syntaxiques LL(<em>k</em>) : si un lexème ne permet pas
de dériver une règle comme il faut, alors l'analyseur peut
<strong>revenir</strong> jusqu'à <em>k</em> étapes en arrière ; nous parlons
aussi de <em lang="en">backtrack</em>. Autrement dit, les règles peuvent être
<strong>ambiguës</strong> : à chaque fois que nous dérivons une règle de la
grammaire, nous avons plusieurs choix possibles et l'analyseur peut se
tromper, c'est pourquoi il doit parfois revenir en arrière. La variable
<em>k</em> permet de définir le <strong>niveau</strong> d'ambiguïté. Si une
grammaire peut être analysée par un analyseur syntaxique LL(1), elle est dite
<strong>non-ambiguë</strong> : à chaque lexème utilisé pour dériver nos
règles, il n'y a qu'un seul choix possible. Et si nous avons un analyseur
syntaxique LL(*), cela signifie que la variable <em>k</em> est
<strong>indéfinie</strong>. L'exemple suivant illustre une grammaire
non-ambiguë :</p>
<pre><code>rule ::= a b c | d e f</code></pre>
<p>Et cet exemple illustre une grammaire ambiguë :</p>
<pre><code>rule1 ::= a rule2
rule2 ::= b rule3 | b rule4
rule3 ::= c d
rule4 ::= e f</code></pre>
<p>Voyons quand nous essayons de trouver une dérivation pour le mot
<code>abef</code> à partir de la règle racine <code>rule1</code> :</p>
<pre><code>rule1
→ a rule2 <em> a bef ✔</em>
→ a (b rule3 | b rule4) <em> a bef</em>
→ a b rule3 <em> ab ef ✔</em>
→ a b c d <em> abe f ✖</em>
← a b rule3 <em> ab ef ✖</em>
← a (b rule3 | b rule4) <em> a bef</em>
→ a b rule4 <em> ab ef ✔</em>
→ a b e f <em>abef ✔</em></code></pre>
<p>La règle <code>rule2</code> est ambiguë, ce qui peut entraîner une mauvaise
dérivation et donc un retour en arrière, un
<em lang="en">backtracking</em>.</p>
<h2 id="LLk_compiler-compiler" for="main-toc">Compilateur de compilateurs
LL(<em>k</em>)</h2>
<p>Écrire des compilateurs est une tâche <strong>laborieuse</strong>. Ce n'est
pas forcément toujours difficile mais souvent répétitif et long. C'est
pourquoi il existe des <strong>compilateurs de compilateurs</strong>, ou
autrement dit, des générateurs de compilateurs. La plupart du temps, ces
compilateurs de compilateurs utilisent un langage
<strong>intermédiaire</strong> pour écrire une grammaire. La bibliothèque
<code>Hoa\Compiler</code> propose la classe <code>Hoa\Compiler\Llk\Llk</code>
qui permet l'écriture de compilateurs de compilateurs à travers un langage
<strong>dédié</strong>.</p>
<h3 id="PP_language" for="main-toc">Langage PP</h3>
<p>Le langage PP, pour <em lang="en">PHP Parser</em>, permet d'exprimer des
<strong>grammaires algébriques</strong>. Il s'écrit dans des fichiers portant
l'extension <code>.pp</code> (voir le fichier
<a href="@central_resource:path=Library/Compiler/.Mime"><code>hoa://Library/Compiler/.Mime</code></a>).</p>
<p>Une grammaire est constituée de <strong>lexèmes</strong> et de
<strong>règles</strong>. La déclaration d'un lexème se fait de la manière
suivante : <code>%token <em>namespace_in</em>:<em>name</em> <em>value</em> ->
<em>namespace_out</em></code>, où <code><em>name</em></code> représente le
<strong>nom</strong> du lexème, <code><em>value</em></code> représente sa
<strong>valeur</strong>, au format <a href="http://pcre.org/">PCRE</a>
(attention à ne pas reconnaître de valeur vide, auquel cas une exception sera
levée), et <code><em>namespace_in</em></code> et
<code><em>namespace_out</em></code> représentent les noms des <strong>espaces
de noms</strong> et sont optionels (vaut <code>default</code> par défaut). Par
exemple <code>number</code> qui représente un nombre composé de chiffres de
<code>0</code> à <code>9</code> :</p>
<pre><code class="language-pp">%token number \d+</code></pre>
<p>Les espaces de noms représentent des <strong>sous-ensembles</strong>
disjoints de lexèmes, utilisés pour <strong>faciliter</strong> les analyses.
Une déclaration <code>%skip</code> est similaire à <code>%token</code>
excepté qu'elle représente un lexème à <strong>sauter</strong>, c'est à dire
à ne pas considérer. Un exemple courant de lexèmes <code>%skip</code> est les
espaces :</p>
<pre><code class="language-pp">%skip space \s</code></pre>
<p>Pour expliquer les règles, nous allons utiliser comme exemple la grammaire
<code>Json.pp</code>, grammaire légèrement <strong>simplifiée</strong> du
<a href="http://json.org/">langage JSON</a> (voir la
<a href="https://tools.ietf.org/html/rfc4627">RFC4627</a>). La grammaire
<strong>complète</strong> se situe dans le fichier
<a href="@central_resource:path=Library/Json/Grammar.pp"><code>hoa://Library/Json/Grammar.pp</code></a>.
Ainsi :</p>
<pre><code class="language-pp">%skip space \s
// Scalars.
%token true true
%token false false
%token null null
// Strings.
%token quote_ " -> string
%token string:string [^"]+
%token string:_quote " -> default
// Objects.
%token brace_ {
%token _brace }
// Arrays.
%token bracket_ \[
%token _bracket \]
// Rest.
%token colon :
%token comma ,
%token number \d+
value:
&amp;lt;true> | &amp;lt;false> | &amp;lt;null> | string() | object() | array() | number()
string:
::quote_:: &amp;lt;string> ::_quote::
number:
&amp;lt;number>
#object:
::brace_:: pair() ( ::comma:: pair() )* ::_brace::
#pair:
string() ::colon:: value()
#array:
::bracket_:: value() ( ::comma:: value() )* ::_bracket::</code></pre>
<p>Nous remarquons que nous avons deux espaces de noms pour les lexèmes :
<code>default</code> et <code>string</code> (cela permet de ne pas
<strong>confondre</strong> les lexèmes <code>quote_</code> et
<code>string:_quote</code> qui ont la même représentation). Nous remarquons
ensuite la règle <code>value</code> qui est une <strong>disjonction</strong>
de plusieurs lexèmes et règles. Les <strong>constructions</strong> du langage
PP sont les suivantes :</p>
<ul>
<li><code><em>rule</em>()</code> pour <strong>appeler</strong> une
règle ;</li>
<li><code>&amp;lt;<em>token</em>></code> et <code>::<em>token</em>::</code>
pour <strong>déclarer</strong> un lexème (les espaces de noms n'apparaissent
pas ici) ;</li>
<li><code>|</code> pour une <strong>disjonction</strong> (un choix) ;</li>
<li><code>(<em>…</em>)</code> pour grouper ;</li>
<li><code><em>e</em>?</code> pour dire que <code><em>e</em></code> est
<strong>optionel</strong> ;</li>
<li><code><em>e</em>+</code> pour dire que <code><em>e</em></code> peut
apparaître <strong>1 ou plusieurs</strong> fois ;</li>
<li><code><em>e</em>*</code> pour dire que <code><em>e</em></code> peut
apparaître <strong>0 ou plusieurs</strong> fois ;</li>
<li><code><em>e</em>{<em>x</em>,<em>y</em>}</code> pour dire que
<code><em>e</em></code> peut apparaître entre <code><em>x</em></code> et
<code><em>y</em></code> fois ;</li>
<li><code>#<em>node</em></code> pour créer un <strong>nœud</strong>
<code><em>node</em></code> dans l'arbre ;</li>
<li><code><em>token</em>[<em>i</em>]</code> pour <strong>unifier</strong>
des lexèmes entre eux.</li>
</ul>
<p>Peu de constructions mais amplement suffisantes.</p>
<p>Enfin, la grammaire du langage PP est écrite… dans le langage PP ! Vous
pourrez la trouver dans le fichier
<a href="@central_resource:path=Library/Compiler/Llk/Llk.pp"><code>hoa://Library/Compiler/Llk/Llk.pp</code></a>.</p>
<h3 id="Compilation_process" for="main-toc">Processus de compilation</h3>
<div id="overview" class="schema"></div>
<script>
Hoa.Document.onReady(function ( ) {
var paper = Hoa.Graph(Hoa.$('#overview'), 800, 360);
var flow = paper.grid(0, 0, 800, 360, 1, 4);
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur lexical'));
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur syntaxique'));
flow.push(paper.rect(0, 0, 200, 70, 3, 'trace'));
flow.push(paper.rect(0, 0, 200, 70, 3, 'AST'));
var annot = paper.grid(180, 0, 80, 360, 1, 4);
annot.push(paper.rect(0, 0, 45, 45, 22.5, '1'));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '2'));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '3'));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '4'));
});
</script>
<p>Le processus de compilation qu'utilise <code>Hoa\Compiler\Llk\Llk</code>
est classique. Il commence par analyser <strong>lexicalement</strong> la
donnée textuelle, le mot, <em>i.e.</em> à transformer notre donnée en une
séquence de lexèmes. L'<strong>ordre</strong> de déclaration des lexèmes est
primordial car l'analyseur lexical va les prendre les uns après les autres.
Ensuite, c'est l'analyseur <strong>syntaxique</strong> qui entre en jeu afin
de <strong>reconnaître</strong> notre donnée.</p>
<p>Si l'analyse syntaxique est un succès, nous obtenons une
<strong>trace</strong>. Cette trace peut être transformée en AST, pour
<em lang="en">Abstract Syntax Tree</em>. Cet arbre représente notre donnée
textuelle après analyse. Il a l'avantage de pouvoir être visité (nous
détaillerons plus loin), ce qui permet par exemple d'ajouter de nouvelles
<strong>contraintes</strong> qui ne peuvent pas être exprimées dans la
grammaire, comme une vérification de type.</p>
<p>Manipulons un peu <code>Hoa\Compiler\Llk\Llk</code>. Cette classe est un
<strong>assistant</strong> pour lire une grammaire au format PP facilement.
Elle prend en seul argument un flux en lecture vers la grammaire et retourne
un compilateur <code>Hoa\Compiler\Llk\Parser</code> prêt à l'emploi. Sur ce
compilateur, nous allons appeler la méthode
<code>Hoa\Compiler\Llk\Parser::parse</code> pour analyser une donnée JSON. Si
la donnée est correcte, nous aurons en retour un AST, sinon une exception sera
levée. Enfin, nous allons utiliser le visiteur
<code>Hoa\Compiler\Visitor\Dump</code> pour afficher notre AST :</p>
<pre><code class="language-php">// 1. Load grammar.
$compiler = Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp'));
// 2. Parse a data.
$ast = $compiler->parse('{"foo": true, "bar": [null, 42]}');
// 3. Dump the AST.
$dump = new Hoa\Compiler\Visitor\Dump();
echo $dump->visit($ast);
/**
* Will output:
* > #object
* > > #pair
* > > > token(string:string, foo)
* > > > token(true, true)
* > > #pair
* > > > token(string:string, bar)
* > > > #array
* > > > > token(null, null)
* > > > > token(number, 42)
*/</code></pre>
<p>Quand nous écrivons et testons une grammaire, nous allons répéter ces trois
tâches très <strong>régulièrement</strong>. C'est pourquoi, le script
<code>hoa</code> propose la commande <code>compiler:pp</code>. Cette commande
propose d'analyser une donnée par rapport à une grammaire et d'appliquer un
visiteur si besoin sur l'AST résultant. Notons que la lecture de la donnée
peut se faire à travers un
<a href="https://en.wikipedia.org/wiki/Pipeline_(Unix)"><em lang="en">pipe</em></a> :</p>
<pre><code class="language-shell">$ echo '[1, [1, [2, 3], 5], 8]' | hoa compiler:pp Json.pp 0 --visitor dump
> #array
> > token(number, 1)
> > #array
> > > token(number, 1)
> > > #array
> > > > token(number, 2)
> > > > token(number, 3)
> > > token(number, 5)
> > token(number, 8)</code></pre>
<p>C'est un moyen pratique pour tester <strong>rapidement</strong> des données
par rapport à notre grammaire. Il ne faut pas hésiter à regarder l'aide de la
commande <code>compiler:pp</code> pour plus de détails !</p>
<p>Les analyses s'effectuent sur la règle <strong>racine</strong> mais nous
pouvons préciser une <strong>autre règle</strong> à l'aide du deuxième
argument de la méthode <code>Hoa\Compiler\Llk\Parser::parse</code> :</p>
<pre><code class="language-php">$compiler->parse('{"foo": true, "bar": [null, 42]}', 'object');</code></pre>
<p>Pour utiliser la règle racine, il suffit de passer <code>null</code>.</p>
<p>Et enfin, pour ne pas générer l'AST mais uniquement savoir si la donnée est
valide ou pas, nous pouvons utiliser le dernier argument de notre méthode en
lui passant <code>false</code> :</p>
<pre><code class="language-php">$valid = $compiler->parse('{"foo": true, "bar": [null, 42]}', null, false);
var_dump($valid);
/**
* Will output:
* bool(true)
*/</code></pre>
<h4 id="Errors" for="main-toc">Erreurs</h4>
<p>Les erreurs de compilation sont remontées à travers des exceptions,
ainsi :</p>
<pre><code class="language-shell">$ echo '{"foo" true}' | hoa compiler:pp Json.pp 0 --visitor dump
Uncaught exception (Hoa\Compiler\Exception\UnexpectedToken):
Hoa\Compiler\Llk\Parser::parse(): (0) Unexpected token "true" (true) at line 1
and column 8:
{"foo" true}
 
in hoa://Library/Compiler/Llk/Parser.php at line 1</code></pre>
<p>Plusieurs exceptions peuvent remonter selon le contexte :</p>
<ul>
<li>durant l'analyse <strong>lexicale</strong>,
<code>Hoa\Compiler\Exception\UnrecognizedToken</code> quand un lexème n'est
pas reconnu, <em>i.e.</em> quand la donnée textuelle ne peut plus être
découpée en une séquence de lexèmes, et
<code>Hoa\Compiler\Exception\Lexer</code> quand d'autres erreurs plus
générales arrivent, par exemple si un lexème reconnaît une valeur
vide ;</li>
<li>durant l'analyse <strong>syntaxique</strong>,
<code>Hoa\Compiler\Exception\UnexpectedToken</code> quand un lexème n'est
pas attendu, <em>i.e.</em> qu'il ne permet plus de dériver les règles de la
grammaire.</li>
</ul>
<p>L'exception parente est <code>Hoa\Compiler\Exception\Exception</code>.</p>
<h4 id="Nodes" for="main-toc">Nœuds</h4>
<p>Le processus de compilation aboutit très souvent à la
<strong>production</strong> d'un AST. Il est important de contrôler sa
<strong>forme</strong>, sa <strong>taille</strong>, les données qu'il
<strong>contient</strong> etc. C'est pourquoi il est nécessaire de comprendre
la notation <code>#<em>node</em></code> car elle permet de créer des
<strong>nœuds</strong> dans l'AST. Une précision tout d'abord, les lexèmes
déclarés avec la syntaxe <code>&amp;lt;<em>token</em>></code> apparaîtront
dans l'arbre, alors que les autres lexèmes, déclarés avec la syntaxe
<code>::<em>token</em>::</code>, n'y apparaîtront pas. En effet, dans notre
dernier exemple, les lexèmes <code>quote_</code>, <code>brace_</code>,
<code>colon</code>, <code>comma</code> etc. n'apparaissent pas. Ensuite, il
est important de noter que les déclarations de nœuds se
<strong>surchargent</strong> les unes par rapport aux autres au sein d'une
<strong>même règle</strong>. Enfin, un nom de règle peut être précédé par
<code>#</code>, comme pour la règle <code>#array</code>, qui permet de définir
un nœud par <strong>défaut</strong>, mais il peut être surchargé. Par exemple,
si nous remplaçons la règle <code>array</code> par :</p>
<pre><code class="language-pp">#array:
::bracket_:: value() ( ::comma:: value() #bigarray )* ::_bracket::</code></pre>
<p>Si le tableau ne contient qu'une seule valeur, le nœud s'appelera
<code>#array</code>, sinon il s'appelera <code>#bigarray</code> ; voyons
plutôt :</p>
<pre><code class="language-shell">$ echo '[42]' | hoa compiler:pp Json.pp 0 --visitor dump
> #array
> > token(number, 42)
$ echo '[4, 2]' | hoa compiler:pp Json.pp 0 --visitor dump
> #bigarray
> > token(number, 4)
> > token(number, 2)</code></pre>
<p>Bien sûr, il peut arriver qu'un nœud soit créé ou pas selon le dérivation
<strong>empruntée</strong> dans la règle. Le mécanisme est normalement assez
<strong>intuitif</strong>.</p>
<h4 id="Namespaces" for="main-toc">Espace de noms</h4>
<p>Détaillons un peu le fonctionnement de l'analyseur lexical vis à vis des
<strong>espaces de noms</strong>.</p>
<p>Les <strong>lexèmes</strong> sont placés dans des espaces de noms,
c'est à dire que seuls les lexèmes de l'espace de noms
<strong>courant</strong> seront utilisés par l'analyseur lexical. L'espace de
noms par défaut est <code>default</code>. Pour déclarer l'espace de noms d'un
lexème, il faut utiliser l'opérateur <code>:</code>. Quand un lexème est
consommé, il peut <strong>changer</strong> l'espace courant pour la suite de
l'analyse lexicale, grâce à l'opérateur <code>-></code>. Ainsi, nous allons
déclarer cinq lexèmes : <code>foo</code> et <code>bar</code> dans l'espace
<code>default</code>, <code>baz</code> dans <code>ns1</code> et
<code>qux</code> et <code>foo</code> dans <code>ns2</code>. Le fait de
déclarer deux fois <code>foo</code> n'est pas gênant car ils sont dans des
espaces de noms <strong>différent</strong> :</p>
<pre><code class="language-pp">%token foo fo+ -> ns1
%token bar ba?r+ -> ns2
%token ns1:baz ba?z+ -> default
%token ns2:qux qux+
%token ns2:foo FOO -> ns1</code></pre>
<p>Écrire <code>default:bar</code> est strictement équivalent à
<code>bar</code>. Le lexème <code>foo</code> emmène dans <code>ns1</code>,
<code>bar</code> dans <code>ns2</code>, <code>ns1:baz</code> dans
<code>default</code>, <code>ns2:qux</code> reste dans <code>ns2</code> et
<code>ns2:foo</code> emmène dans <code>ns1</code>. Observons la séquence de
lexèmes produite par l'analyseur lexical avec la donnée
<code>fooooobzzbarrrquxFOObaz</code> :</p>
<pre><code class="language-php">$pp = '%token foo fo+ -> ns1' . "\n" .
'%token bar ba?r+ -> ns2' . "\n" .
'%token ns1:baz ba?z+ -> default' . "\n" .
'%token ns2:qux qux+' . "\n" .
'%token ns2:foo FOO -> ns1';
// 1. Parse PP.
Hoa\Compiler\Llk\Llk::parsePP($pp, $tokens, $rawRules);
// 2. Run the lexical analyzer.
$lexer = new Hoa\Compiler\Llk\Lexer();
$sequence = $lexer->lexMe('fooooobzzbarrrquxFOObaz', $tokens);
// 3. Pretty-print the result.
$format = '%' . (strlen((string) count($sequence)) + 1) . 's ' .
'%-13s %-20s %-20s %6s' . "\n";
$header = sprintf($format, '#', 'namespace', 'token name', 'token value', 'offset');
echo $header, str_repeat('-', strlen($header)), "\n";
foreach ($sequence as $i => $token) {
printf(
$format,
$i,
$token['namespace'],
$token['token'],
$token['value'],
$token['offset']
);
}
/**
* Will output:
* # namespace token name token value offset
* ---------------------------------------------------------------------
* 0 default foo fooooo 0
* 1 ns1 baz bzz 6
* 2 default bar barrr 9
* 3 ns2 qux qux 14
* 4 ns2 foo FOO 17
* 5 ns1 baz baz 20
* 6 default EOF EOF 23
*/</code></pre>
<p>Nous lisons les lexèmes, leur espace et leur valeur dans le tableau. La
donnée a pu être <strong>découpée</strong>, et nous sommes passés d'un espace
à un autre. Si cette fois nous essayons avec la donnée <code>foqux</code>,
nous aurons une erreur : <code>fo</code> correspond au lexème <code>foo</code>
dans l'espace <code>default</code>, nous changeons alors d'espace pour
<code>ns1</code>, et là, aucun lexème dans cet espace ne peut reconnaître au
moins <code>q</code>. Une erreur sera levée.</p>
<p>Jusqu'à maintenant, nous avons vu comment passer d'un espace à l'autre avec
l'opérateur <code>-></code>. Aucun <strong>historique</strong> sur les espaces
traversés n'est conservé. Toutefois, dans certains cas rares, il peut arriver
qu'un espace de lexèmes soit accessible via <strong>plusieurs</strong> autres
et que nous aimerions qu'un lexème déclenche le retour vers l'espace de noms
<strong>précédent</strong>. Autrement dit, nous aimerions un historique des
espaces traversés et pouvoir y naviguer (en arrière uniquement). C'est le rôle
du mot-clé <code>__shift__ * <em>n</em></code>, à utiliser après l'opérateur
<code>-></code> et à la place du nom d'un espace. <code>__shift__</code> est
équivalent à dire : revient à l'espace précédent. <code>__shift__</code> est
équivalent à <code>__shift__ * 1</code>, et
<code>__shift__ * <em>n</em></code> à : revient <code><em>n</em></code> fois à
l'espace précédent.</p>
<p>Lorsque le mot-clé <code>__shift__</code> apparaît dans la grammaire, les
espaces sont gérés comme une <strong>pile</strong>, d'où le vocabulaire
<em lang="en">shift</em>. Il faut faire attention à bien dépiler les espaces
pour ne pas avoir une pile trop conséquente.</p>
<p>Prenons en exemple la grammaire suivante :</p>
<pre><code class="language-pp">%token foo1 a -> ns1
%token foo2 x -> ns2
%token end e
%token ns1:bar b
%token ns1:baz c -> ns3
%token ns1:tada t -> __shift__
%token ns2:bar y
%token ns2:baz z -> ns3
%token ns2:tada T -> __shift__
%token ns3:qux = -> __shift__
#test:
( &amp;lt;foo1> | &amp;lt;foo2> ) &amp;lt;bar> &amp;lt;baz> &amp;lt;qux> &amp;lt;tada> &amp;lt;end></code></pre>
<p>Cette grammaire reconnaît deux données : <code>abc=te</code> et
<code>xyz=Te</code>. Si le premier lexème <code>foo1</code> est rencontré,
l'analyseur syntaxique changera d'espace pour <code>ns1</code>. Quand il
rencontrera le lexème <code>ns1:baz</code>, il passera dans <code>ns3</code>.
Ensuite, quand il rencontrera <code>ns3:qux</code>, il reviendra à l'espace
précédent, soit <code>ns1</code>. Et ainsi de suite. Maintenant, s'il ne
rencontre pas <code>foo1</code> mais <code>foo2</code>, il ira dans l'espace
<code>ns2</code>, puis dans <code>ns3</code> puis à nouveau <code>ns2</code>.
Les mots-clés <code>__shift__</code> pour <code>ns1:tada</code> et
<code>ns2:tada</code> n'ont pas d'autres buts que de dépiler les espaces, mais
aucune ambiguïté n'existe : ces espaces ne sont accessibles que par un seul
espace, à savoir <code>default</code>.</p>
<p>Maintenant, enregistrons cette grammaire dans un fichier
<code>NamespaceStack.pp</code> et utilisons l'option
<code>-s/--token-sequence</code> de la commande <code>hoa
compiler:pp</code> :</p>
<pre><code class="language-shell">$ echo -n 'abc=te' | hoa compiler:pp NamespaceStack.pp 0 --token-sequence
# namespace token name token value offset
-------------------------------------------------------------------------------
0 default foo1 a 0
1 ns1 bar b 1
2 ns1 baz c 2
3 ns3 qux = 3
4 ns1 tada t 4
5 default end e 5
6 default EOF EOF 6
$ echo -n 'xyz=Te' | hoa compiler:pp NamespaceStack.pp 0 --token-sequence
# namespace token name token value offset
-------------------------------------------------------------------------------
0 default foo2 x 0
1 ns2 bar y 1
2 ns2 baz z 2
3 ns3 qux = 3
4 ns2 tada T 4
5 default end e 5
6 default EOF EOF 6</code></pre>
<p>Nous voyons que l'analyse lexicale a réussi à jongler avec les espaces de
noms, comme attendu. Nous avions deux façons d'accéder à l'espace
<code>ns3</code> : soit depuis <code>ns1</code>, soit depuis <code>ns2</code>.
L'analyseur a réussi à créer un historique des espaces et à y naviguer.</p>
<h4 id="Unification" for="main-toc">Unification</h4>
<p>Une caractéristique qu'apporte le langage PP par rapport à d'autres
langages de grammaires connus est la capacité d'exprimer une
<strong>unification</strong> de lexèmes. Imaginons la grammaire
suivante :</p>
<pre><code class="language-pp">%token quote '|"
%token string \w+
rule:
::quote:: &amp;lt;string> ::quote::</code></pre>
<p>Les guillemets qui entourent la chaîne de caractère peuvent être de deux
sortes : simple, avec le symbole « <code>'</code> », ou double, avec le
symbole « <code>"</code> ». Ainsi, les données <code>"foo"</code> et
<code>'foo'</code> sont valides, mais <strong>également</strong>
<code>"foo'</code> et <code>'foo"</code> !</p>
<p>L'unification des lexèmes permet d'ajouter une <strong>contrainte</strong>
supplémentaire sur la <strong>valeur</strong> des lexèmes à l'exécution. La
syntaxe est la suivante : <code><em>token</em>[<em>i</em>]</code>. La valeur
de <code><em>i</em></code> indique quels lexèmes vont devoir porter la même
valeur. Et enfin, l'unification est <strong>locale</strong> à une instance
d'une règle, c'est à dire qu'il n'y a pas d'unification entre les lexèmes de
plusieurs règles et que cela s'applique sur la règle <strong>appelée</strong>
uniquement. Ainsi, l'exemple devient :</p>
<pre><code class="language-pp">rule:
::quote[0]:: &amp;lt;string> ::quote[0]::</code></pre>
<p>Ce qui invalide les données <code>"foo'</code> et <code>'foo"</code>.</p>
<p>L'unification trouve de nombreuses applications, comme par exemple unifier
les noms des balises ouvrantes et fermantes du
<a href="http://w3.org/TR/xml11/">langage XML</a>.</p>
<h3 id="Abstract_Syntax_Tree" for="main-toc"><em lang="en">Abstract Syntax
Tree</em></h3>
<div id="overview_ast" class="schema"></div>
<script>
Hoa.Document.onReady(function ( ) {
var paper = Hoa.Graph(Hoa.$('#overview_ast'), 800, 360);
var flow = paper.grid(0, 0, 800, 360, 1, 4);
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur lexical').attr({opacity: .3}));
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur syntaxique').attr({opacity: .3}));
flow.push(paper.rect(0, 0, 200, 70, 3, 'trace').attr({opacity: .3}));
flow.push(paper.rect(0, 0, 200, 70, 3, 'AST'));
var annot = paper.grid(180, 0, 80, 360, 1, 4);
annot.push(paper.rect(0, 0, 45, 45, 22.5, '1').attr({opacity: .3}));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '2').attr({opacity: .3}));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '3').attr({opacity: .3}));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '4'));
});
</script>
<p>Un arbre <strong>syntaxique abstrait</strong> représente une donnée
textuelle sous forme <strong>structurelle</strong>. Chaque
<strong>nœud</strong> de cet arbre est représenté par la classe
<code>Hoa\Compiler\Llk\TreeNode</code>. Parmis les méthodes utiles, nous
trouvons :</p>
<ul>
<li><code>getId</code> pour obtenir l'identifiant du nœud ;</li>
<li><code>getValueToken</code> pour obtenir le nom du lexème ;</li>
<li><code>getValueValue</code> pour obtenir la valeur du lexème ;</li>
<li><code>isToken</code> si le nœud représente un lexème ;</li>
<li><code>getChild(<em>$i</em>)</code> pour obtenir le
<code><em>$i</em></code>-ème enfant d'un nœud ;</li>
<li><code>getChildren</code> pour obtenir tous les nœuds ;</li>
<li><code>getChildrenNumber</code> pour connaître le nombre d'enfants ;</li>
<li><code>getParent</code> pour obtenir le nœud parent ;</li>
<li><code>getData</code> pour obtenir une référence vers un tableau de
données ;</li>
<li><code>accept</code> pour appliquer un visiteur.</li>
</ul>
<p>Les visiteurs sont le moyen le plus pratique pour
<strong>parcourir</strong> un AST. En guise d'exemple, nous allons écrire le
visiteur <code>PrettyPrinter</code> qui va réécrire une donnée JSON avec notre
propre convention (espacements, retours à la ligne etc.). Un visiteur doit
implémenter l'interface <code>Hoa\Visitor\Visit</code> et n'aura qu'une seule
méthode à écrire : <code>visit</code> qui prend trois arguments :
l'élément et deux arguments accessoires (en référence et en copie). Voyons
plutôt :</p>
<pre><code class="language-php">class PrettyPrinter implements Hoa\Visitor\Visit
{
public function visit (
Hoa\Visitor\Element $element,
&amp;amp;$handle = null,
$eldnah = null
) {
static $i = 0;
static $indent = ' ';
// One behaviour per node in the AST.
switch ($element->getId()) {
// Object: { … }.
case '#object':
echo '{', "\n";
++$i;
foreach ($element->getChildren() as $e => $child) {
if (0 &amp;lt; $e) {
echo ',', "\n";
}
echo str_repeat($indent, $i);
$child->accept($this, $handle, $eldnah);
}
echo "\n", str_repeat($indent, --$i), '}';
break;
// Array: [ … ].
case '#array':
echo '[', "\n";
++$i;
foreach ($element->getChildren() as $e => $child) {
if (0 &amp;lt; $e) {
echo ',', "\n";
}
echo str_repeat($indent, $i);
$child->accept($this, $handle, $eldnah);
}
echo "\n", str_repeat($indent, --$i), ']';
break;
// Pair: "…": ….
case '#pair':
echo
$element->getChild(0)->accept($this, $handle, $eldnah),
': ',
$element->getChild(1)->accept($this, $handle, $eldnah);
break;
// Many tokens.
case 'token':
switch ($element->getValueToken()) {
// String: "…".
case 'string':
echo '"', $element->getValueValue(), '"';
break;
// Booleans.
case 'true':
case 'false':
// Null.
case 'null':
// Number.
case 'number':
echo $element->getValueValue();
break;
}
break;
}
}
}</code></pre>
<p>Nous allons voir tout de suite un exemple d'utilisation :</p>
<pre><code class="language-php">$compiler = Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp'));
$ast = $compiler->parse('{"foo": true, "bar": [null, 42]}');
$prettyPrint = new PrettyPrinter();
echo $prettyPrint->visit($ast);
/**
* Will output:
* {
* "foo": true,
* "bar": [
* null,
* 42
* ]
* }
*/</code></pre>
<p>La méthode <code>getData</code> est très pratique pour
<strong>stocker</strong> des données susceptibles d'être
<strong>réutilisées</strong>, par exemple d'un visiteur à l'autre. Cette
méthode retourne une <strong>référence</strong> sur un tableau ; ainsi :</p>
<pre><code class="language-php">$data = $element->getData();
if (!isset($data['previousComputing'])) {
throw new Exception('Need a previous computing.', 0);
}
$previous = $data['previousComputing'];</code></pre>
<p>Il est courant d'utiliser un visiteur par <strong>contrainte</strong> :
vérifiation des symboles, vérification de types etc. Certains peuvent laisser
des données nécessaires pour le suivant. La méthode <code>getData</code>
n'impose aucune structuration des données, elle propose uniquement un accès à
un tableau. Ce sera à vous de faire le reste.</p>
<p>Utiliser la classe <code>Hoa\Compiler\Llk\TreeNode</code> est vraiment
<strong>trivial</strong> et nous l'utiliserons la plupart du temps avec un
visiteur.</p>
<h3 id="Traces" for="main-toc">Traces</h3>
<div id="overview_trace" class="schema"></div>
<script>
Hoa.Document.onReady(function ( ) {
var paper = Hoa.Graph(Hoa.$('#overview_trace'), 800, 360);
var flow = paper.grid(0, 0, 800, 360, 1, 4);
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur lexical').attr({opacity: .3}));
flow.push(paper.rect(0, 0, 200, 70, 3, 'analyseur syntaxique').attr({opacity: .3}));
flow.push(paper.rect(0, 0, 200, 70, 3, 'trace'));
flow.push(paper.rect(0, 0, 200, 70, 3, 'AST').attr({opacity: .3}));
var annot = paper.grid(180, 0, 80, 360, 1, 4);
annot.push(paper.rect(0, 0, 45, 45, 22.5, '1').attr({opacity: .3}));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '2').attr({opacity: .3}));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '3'));
annot.push(paper.rect(0, 0, 45, 45, 22.5, '4').attr({opacity: .3}));
});
</script>
<p>Le compilateur LL(<em>k</em>) que propose Hoa est clairement découpé en
plusieurs <strong>couches</strong>. Chaque couche est exploitable pour
permettre une modularité maximum. Quand la grammaire est traduite en « règles
machines » et que les analyseurs lexical et syntaxique ont validé une donnée,
il en résulte une <strong>trace</strong>. Cette trace est un tableau composé
de trois classes seulement :</p>
<ul>
<li><code>Hoa\Compiler\Llk\Rule\Entry</code> quand l'analyseur syntaxique
est rentré dans une règle ;</li>
<li><code>Hoa\Compiler\Llk\Rule\Ekzit</code> quand l'analyseur syntaxique
est sorti d'une règle ;</li>
<li><code>Hoa\Compiler\Llk\Rule\Token</code> quand l'analyseur syntaxique a
rencontré un lexème.</li>
</ul>
<p>Nous pouvons l'obtenir grâce à la méthode
<code>Hoa\Compiler\Llk\Parser::getTrace</code>. Pour bien comprendre cette
trace, nous allons commencer par un exemple :</p>
<pre><code class="language-php">$compiler = Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp'));
$ast = $compiler->parse('{"foo": true, "bar": [null, 42]}');
$i = 0;
foreach ($compiler->getTrace() as $element) {
if ($element instanceof Hoa\Compiler\Llk\Rule\Entry) {
echo str_repeat('> ', ++$i), 'enter ', $element->getRule(), "\n";
} elseif ($element instanceof Hoa\Compiler\Llk\Rule\Token) {
echo
str_repeat(' ', $i + 1), 'token ', $element->getTokenName(),
', consumed ', $element->getValue(), "\n";
} else {
echo str_repeat('&amp;lt; ', $i--), 'ekzit ', $element->getRule(), "\n";
}
}
/**
* Will output:
* > enter value
* > > enter object
* token brace_, consumed {
* > > > enter pair
* > > > > enter string
* token quote_, consumed "
* token string, consumed foo
* token _quote, consumed "
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit string
* token colon, consumed :
* > > > > enter value
* token true, consumed true
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit value
* &amp;lt; &amp;lt; &amp;lt; ekzit pair
* > > > enter 13
* > > > > enter 12
* token comma, consumed ,
* > > > > > enter pair
* > > > > > > enter string
* token quote_, consumed "
* token string, consumed bar
* token _quote, consumed "
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit string
* token colon, consumed :
* > > > > > > enter value
* > > > > > > > enter array
* token bracket_, consumed [
* > > > > > > > > enter value
* token null, consumed null
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit value
* > > > > > > > > enter 21
* > > > > > > > > > enter 20
* token comma, consumed ,
* > > > > > > > > > > enter value
* token number, consumed 42
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit value
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit 20
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit 21
* token _bracket, consumed ]
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit array
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit value
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit pair
* &amp;lt; &amp;lt; &amp;lt; &amp;lt; ekzit 12
* &amp;lt; &amp;lt; &amp;lt; ekzit 13
* token _brace, consumed }
* &amp;lt; &amp;lt; ekzit object
* &amp;lt; ekzit value
*/</code></pre>
<p>Cet exemple nous révèle plusieurs choses. Tout d'abord, les informations
que nous donne la trace peuvent être utiles : si nous sommes sur une règle,
nous avons son <strong>nom</strong> (avec la méthode <code>getRule</code>), et
si nous sommes sur un lexème, nous avons son <strong>nom</strong> (avec la
méthode <code>getTokenName</code>), sa <strong>représentation</strong> (sous
la forme d'une PCRE, avec la méthode <code>getRepresentation</code>), sa
<strong>valeur</strong> (avec la méthode <code>getValue</code>), si c'est un
nœud à <strong>conserver</strong> dans l'AST (avec la méthode
<code>isKept</code>) et son index d'<strong>unification</strong> s'il existe
(avec la méthode <code>getUnificationIndex</code>). Bien sûr, tout ceci est
modifiable, ce qui peut influencer la construction de l'AST qui est le
processus (optionnel) suivant.</p>
<p>Ensuite, nous remarquons que parfois nous entrons dans une règle qui existe
dans la grammaire, comme <code>object</code>, <code>pair</code>,
<code>value</code> etc., et parfois nous entrons dans une règle
<strong>numérique</strong>, comme <code>13</code>, <code>12</code>,
<code>21</code>, <code>20</code> etc. Quand la grammaire est interprétée pour
être transformée en « règles machines », chacune de ses règles est
<strong>linéarisée</strong> selon les opérateurs du langage PP :</p>
<ul>
<li><code>Hoa\Compiler\Llk\Rule\Choice</code> pour une disjonction ;</li>
<li><code>Hoa\Compiler\Llk\Rule\Concatenation</code> pour une
concaténation ;</li>
<li><code>Hoa\Compiler\Llk\Rule\Repetition</code> pour une répétition ;</li>
<li><code>Hoa\Compiler\Llk\Rule\Token</code> pour un lexème (déjà vu
précédemment).</li>
</ul>
<p>Toutes les règles sous ce format sont accessibles à travers la méthode
<code>Hoa\Compiler\Llk\Parser::getRules</code> sous la forme d'un tableau.
Nous allons afficher toutes les règles <strong>accessibles</strong> depuis la
règle racine pour mieux comprendre :</p>
<pre><code class="language-php">$compiler = Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp'));
// 1. Get all rules.
$rules = $compiler->getRules();
// 2. Start with the root rule.
$stack = [$compiler->getRootRule() => true];
while (false !== current($stack)) {
$rule = key($stack);
next($stack);
echo
"\n", '"', $rule, '" is a ',
strtolower(substr(get_class($rules[$rule]), 22));
$subrules = $rules[$rule]->getContent();
// 3a. Token.
if (null === $subrules) {
continue;
}
echo ' of rules: ';
// 3b. Other rules.
foreach ((array) $rules[$rule]->getContent() as $subrule) {
if (!array_key_exists($subrule, $stack)) {
// 4. Unshift new rules to print.
$stack[$subrule] = true;
}
echo $subrule, ' ';
}
}
/**
* Will output:
* "value" is a choice of rules: 1 2 3 string object array number
* "1" is a token
* "2" is a token
* "3" is a token
* "string" is a concatenation of rules: 5 6 7
* "object" is a concatenation of rules: 10 pair 13 14
* "array" is a concatenation of rules: 18 value 21 22
* "number" is a token
* "5" is a token
* "6" is a token
* "7" is a token
* "10" is a token
* "pair" is a concatenation of rules: string 16 value
* "13" is a repetition of rules: 12
* "14" is a token
* "18" is a token
* "21" is a repetition of rules: 20
* "22" is a token
* "16" is a token
* "12" is a concatenation of rules: 11 pair
* "20" is a concatenation of rules: 19 value
* "11" is a token
* "19" is a token
*/</code></pre>
<p>Si nous lisons la règle <code>object</code>, nous savons que c'est la
concaténation des règles <code>10</code>, <code>pair</code>, <code>13</code>
et <code>14</code>. <code>10</code> est un lexème, <code>pair</code> est la
concaténation des règles <code>string</code>, <code>16</code> et
<code>value</code>, et ainsi de suite. La grammaire initiale est transformée
pour être sous sa forme la plus <strong>réduite</strong> possible. Ceci permet
de <strong>raisonner</strong> beaucoup plus <strong>facilement</strong> et
<strong>rapidement</strong> sur les règles. En effet, les traitements sur la
grammaire ne sont pas réservés aux AST. À l'étape précédente, avec la trace,
nous pouvons déjà effectuer des traitements.</p>
<h3 id="Generation" for="main-toc">Génération</h3>
<p>Une grammaire peut être utile pour deux objectifs :
<strong>valider</strong> une donnée (si nous la voyons comme un automate) ou
<strong>générer</strong> des données. Jusqu'à présent, nous avons vu comment
valider une donnée à travers plusieurs processus :
<strong>préparation</strong> de notre compilateur, exécution des
<strong>analyseurs</strong> lexical et syntaxique résultant sur une
<strong>trace</strong>, transformation de la trace vers un
<strong>AST</strong> pour enfin <strong>visiter</strong> cet arbre. Mais nous
pouvons nous arrêter à la première étape, la préparation de notre compilateur,
pour exploiter les règles afin de générer une donnée qui sera valide par
rapport à notre grammaire.</p>
<p><code>Hoa\Compiler\Llk\Sampler</code> propose trois algorithmes de
<strong>générations</strong> de données :</p>
<ul>
<li>génération aléatoire et uniforme ;</li>
<li>génération exhaustive bornée ;</li>
<li>génération basée sur la couverture.</li>
</ul>
<p>Pourquoi proposer trois algorithmes ? Parce qu'il est illusoire de penser
qu'un seul algorithme peut suffir aux <strong>nombreux</strong> contextes
d'utilisations. Chacun répond à des besoins différents, nous l'expliquerons
plus loin.</p>
<p>Générer une donnée à partir d'une grammaire se fait en <strong>deux
étapes</strong> :</p>
<ol>
<li>générer des valeurs pour les <strong>lexèmes</strong> afin d'avoir des données
brutes ;</li>
<li>générer un <strong>chemin</strong> dans les règles de la grammaire.</li>
</ol>
<p>Un chemin est équivalent à une dérivation, le vocabulaire est différent
selon notre objectif : validation ou génération.</p>
<h4 id="Isotropic_generation_of_lexemes" for="main-toc">Génération
isotropique de lexèmes</h4>
<p>Pour générer les valeurs des lexèmes, peu importe l'algorithme utilisé, il
doit être <strong>rapide</strong>. Nous allons utiliser un parcours dit
<strong>isotrope</strong>. Nous partons d'une règle et nous avançons
uniquement à partir de choix <strong>aléatoires</strong> et <strong>uniformes
localement</strong> (uniquement pour ce choix). Par exemple si nous avons une
disjonction entre trois sous-règles, nous allons tirer aléatoirement et
uniformément entre 1 et 3. Si nous avons une concaténation, nous allons juste
concaténer chaque partie. Et enfin, une répétition n'est rien d'autre qu'une
disjonction de concaténation : en effet, <code><em>e</em>{1,3}</code> est
strictement équivalent à <code><em>e</em> | <em>ee</em> | <em>eee</em></code>.
Illustrons plutôt :</p>
<pre><code>([ae]+|[x-z]!){1,3} <em>repeat <em>[ae]+|[x-z]!</em> 2 times</em>
→ ([ae]+|[x-z]!)([ae]+|[x-z]!) <em>choose between <em>[ae]+</em> and <em>[x-z]!</em></em>
→ ([ae]+)([ae]+|[x-z]!) <em>repeat <code>ae</code> 2 times</em>
→ [ae][ae]([ae]+|[x-z]!) <em>choose between <em>a</em> and <em>e</em></em>
→ e[ae]([ae]+|[x-z]!) <em>again</em>
→ ea([ae]+|[x-z]!) <em>choose between <em>[ae]+</em> and <em>[x-z]!</em></em>
→ ea([x-z]!) <em>choose between <em>x</em>, <em>y</em> and <em>z</em></em>
→ eay!</code></pre>
<p>Cette génération est <strong>naïve</strong> mais ce n'est pas important. Ce
qui est vraiment important est la génération des chemins dans les règles, ou
autrement dit, la génération des <strong>séquences de lexèmes</strong>.</p>
<h4 id="Uniform_random_generation" for="main-toc">Génération aléatoire
et uniforme</h4>
<p>Le premier algorithme est celui de la génération <strong>aléatoire</strong>
et <strong>uniforme</strong>. Cet algorithme est utile si nous voulons générer
des séquences de lexèmes de <strong>taille <em>n</em> fixe</strong> et avec
une <strong>uniformité</strong> non pas locale (comme avec un parcours
isotrope) mais sur l'<strong>ensemble</strong> de toutes les séquences
possibles. Succinctement, l'algorithme travaille en deux étapes :
<strong>pré-calcul</strong> (une seule fois par taille) puis
<strong>génération</strong>. Le pré-calcul est une étape
<strong>automatique</strong> et calcule le <strong>nombre</strong> de
séquences et sous-séquences possibles de taille <em>n</em>. Cette étape aide
au calcul de <strong>fonctions de répartions</strong> pour
<strong>guider</strong> la génération des séquences de lexèmes finales.</p>
<p>Nous allons générer 10 données aléatoires de taille 7, c'est à dire
composées de 7 lexèmes. Pour cela, nous allons utiliser la classe
<code>Hoa\Compiler\Llk\Sampler\Uniform</code> qui prend en premier argument
notre grammaire, en deuxième le générateur de valeurs de lexèmes (ici
<code>Hoa\Regex\Visitor\Isototropic</code>) et enfin la taille :</p>
<pre><code class="language-php">$sampler = new Hoa\Compiler\Llk\Sampler\Uniform(
// Grammar.
Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp')),
// Token sampler.
new Hoa\Regex\Visitor\Isotropic(new Hoa\Math\Sampler\Random()),
// Length.
7
);
for ($i = 0; $i &amp;lt; 10; ++$i) {
echo $i, ' => ', $sampler->uniform(), "\n";
}
/**
* Will output:
* 0 => [ false , null , null ]
* 1 => [ " l " , null ]
* 2 => [ [ true ] , true ]
* 3 => [ [ [ 4435 ] ] ]
* 4 => [ [ [ 9366 ] ] ]
* 5 => [ true , false , null ]
* 6 => { " |h&amp;lt;# " : false }
* 7 => [ [ [ false ] ] ]
* 8 => [ false , true , 7 ]
* 9 => [ false , 5 , 79 ]
*/</code></pre>
<p>Nous pouvons redéfinir la taille avec la méthode
<code>Hoa\Compiler\Llk\Sampler\Uniform::setLength</code>. Nous aurons
peut-être remarqué que certains opérateurs de répétition n'ont pas de bornes
supérieures, comme <code>+</code> ou <code>*</code> ; dans ce cas, nous la
définissons à <em>n</em>.</p>
<h4 id="Bounded_exhaustive_generation" for="main-toc">Génération exhaustive
bornée</h4>
<p>Le deuxième algorithme est celui de la génération <strong>exhaustive
bornée</strong>. Cet algorithme génère <strong>toutes</strong> les séquences
(c'est le côté exhaustif) de lexèmes de taille 1 <strong>jusqu'à</strong>
<em>n</em> (c'est le caractère borné). Un aspect très intéressant de cet
algorithme est que l'exhaustivité est une forme de <strong>preuve</strong> !
L'algorithme fonctionne comme un itérateur et est très simple à utiliser :</p>
<pre><code class="language-php">$sampler = new Hoa\Compiler\Llk\Sampler\BoundedExhaustive(
// Grammar.
Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp')),
// Token sampler.
new Hoa\Regex\Visitor\Isotropic(new Hoa\Math\Sampler\Random()),
// Length.
7
);
foreach ($sampler as $i => $data) {
echo $i, ' => ', $data, "\n";
}
/**
* Will output:
* 0 => true
* 1 => false
* 2 => null
* 3 => " 8u2 "
* 4 => { " ,M@aj{ " : true }
* 5 => { " x`|V " : false }
* 6 => { " NttB " : null }
* 7 => { " eJWwA " : 0 }
* 8 => [ true ]
* 9 => [ true , true ]
* 10 => [ true , true , true ]
* 11 => [ true , true , false ]
* 12 => [ true , true , null ]
* 13 => [ true , true , 32 ]
* 14 => [ true , false ]
* 15 => [ true , false , true ]
* 16 => [ true , false , false ]
* 17 => [ true , false , null ]
* 18 => [ true , false , 729 ]
* 19 => [ true , null ]
* 20 => [ true , null , true ]
* 21 => [ true , null , false ]
* …
* 157 => [ 780 , 01559 , 32 ]
* 158 => 344
*/</code></pre>
<p><em>A l'instar</em> de l'algorithme précédent, nous pouvons redéfinir la
borne maximum avec la méthode
<code>Hoa\Compiler\Llk\Sampler\BoundedExhaustive::setLength</code>. Et les
opérateurs de répétition sans borne supérieure utilisent <em>n</em>.</p>
<h4 id="Grammar_coverage-based_generation" for="main-toc">Génération basée
sur la couverture</h4>
<p>Le dernier algorithme est celui de la génération <strong>basée sur la
couverture</strong>. Cet algorithme réduit l'<strong>explosion
combinatoire</strong> rencontrée avec l'algorithme précédent mais l'objectif
est tout autre : il va générer des séquences de lexèmes qui vont
« <strong>activer</strong> » toutes les <strong>branches</strong> des règles
de la grammaire. Une règle est dite couverte si et seulement si ses
sous-règles sont toutes couvertes, et un lexème est dit couvert s'il a été
utilisé dans une séquence. Pour assurer une <strong>diversité</strong> dans
les séquences produites, les points de choix entre des sous-règles non
couvertes sont résolus par tirages <strong>aléatoires</strong>. L'algorithme
fonctionne également comme un itérateur :</p>
<pre><code class="language-php">$sampler = new Hoa\Compiler\Llk\Sampler\Coverage(
// Grammar.
Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp')),
// Token sampler.
new Hoa\Regex\Visitor\Isotropic(new Hoa\Math\Sampler\Random())
);
foreach ($sampler as $i => $data) {
echo $i, ' => ', $data, "\n";
}
/**
* Will output:
* 0 => true
* 1 => { " )o?bz " : null , " %3W) " : [ false , 130 , " 6 " ] }
* 2 => [ { " ny " : true } ]
* 3 => { " Ne;[3 " : [ true , true ] , " th: " : true , " C[8} " : true }
*/</code></pre>
<p>Pour éviter l'explosion combinatoire et assurer la
<strong>termination</strong> de l'algorithme, nous utilisons
l'<strong>heuristique</strong> suivante sur les opérateurs de
<strong>répétition</strong> : <code>*</code> répétera <code>0</code>,
<code>1</code> et <code>2</code> fois, <code>+</code> répétera <code>1</code>
et <code>2</code> fois, et enfin <code>{<em>x</em>,<em>y</em>}</code>
répétera <code><em>x</em></code>, <code><em>x</em> + 1</code>,
<code><em>y</em> - 1</code> et <code><em>y</em></code> fois. Cette heuristique
trouve ses origines dans le test aux <strong>limites</strong>.</p>
<p>Nous remarquons dans notre exemple que 4 données sont générées et suffisent
à <strong>couvrir</strong> l'ensemble de notre grammaire !</p>
<h4 id="Comparison_between_algorithms" for="main-toc">Comparaison entre
les algorithmes</h4>
<p>Voici quelques <strong>indices</strong> pour savoir quand utiliser tel ou
tel autre algorithme.</p>
<dl>
<dt>Génération aléatoire et uniforme :</dt>
<dd><ul>
<li data-item="+">rapide pour des petites données, grande diversité dans
les données et taille fixe ;</li>
<li data-item="-">la phase de pré-calcul est exponentielle et donc
longue bien que, ensuite, la génération soit très rapide.</li>
</ul></dd>
<dt>Génération exhaustive bornée :</dt>
<dd><ul>
<li data-item="+">rapide pour des petites données et l'exhaustivité est
efficace ;</li>
<li data-item="-">nombre exponentiel de données.</li>
</ul></dd>
<dt>Génération basée sur la couverture :</dt>
<dd><ul>
<li data-item="+">rapide pour des données de taille moyenne ou grande,
et diversité des données ;</li>
<li data-item="-">ne considère pas de taille.</li>
</ul></dd>
</dl>
<h2 id="LL1_compiler-compiler" for="main-toc">Compilateur de compilateurs
LL(1)</h2>
<p>La documentation pour le compilateur LL(1), à travers la classe
<code>Hoa\Compiler\Ll1</code>, n'est pas encore écrite. L'objectif est
différent de <code>Hoa\Compiler\Llk</code> : il n'existe pas de langage
intermédiaire, les automates sont codés en dur avec des tableaux et ce type de
compilateurs est orienté haute performance. C'est pourquoi son comportement
est <strong>monolithique</strong>.</p>
<h2 id="Conclusion" for="main-toc">Conclusion</h2>
<p><code>Hoa\Compiler</code> propose deux <strong>compilateurs de
compilateurs</strong> : <code>Hoa\Compiler\Llk</code> et
<code>Hoa\Compiler\Ll1</code>, afin de <strong>valider</strong> des données.
Le <strong>langage PP</strong> permet d'écrire des <strong>grammaires
algébriques</strong> de manière <strong>simple</strong> et
<strong>naturelle</strong>. Le compilateur de compilateurs LL(<em>k</em>) est
découpé en <strong>processus distincts</strong> ce qui le rend très
<em lang="en">hackable</em>. Enfin, différents algorithmes permettent de
<strong>générer</strong> des données à partir d'une grammaire selon le
contexte d'utilisation.</p>
</yield>
</overlay>