mirror of
https://github.com/zoe-may/TDoG-Skin.git
synced 2025-01-19 22:07:24 +08:00
1288 lines
62 KiB
Plaintext
1288 lines
62 KiB
Plaintext
|
<?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:
|
|||
|
&lt;true> | &lt;false> | &lt;null> | string() | object() | array() | number()
|
|||
|
|
|||
|
string:
|
|||
|
::quote_:: &lt;string> ::_quote::
|
|||
|
|
|||
|
number:
|
|||
|
&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>&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>&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:
|
|||
|
( &lt;foo1> | &lt;foo2> ) &lt;bar> &lt;baz> &lt;qux> &lt;tada> &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:: &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]:: &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;$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 &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 &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('&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 "
|
|||
|
* &lt; &lt; &lt; &lt; ekzit string
|
|||
|
* token colon, consumed :
|
|||
|
* > > > > enter value
|
|||
|
* token true, consumed true
|
|||
|
* &lt; &lt; &lt; &lt; ekzit value
|
|||
|
* &lt; &lt; &lt; ekzit pair
|
|||
|
* > > > enter 13
|
|||
|
* > > > > enter 12
|
|||
|
* token comma, consumed ,
|
|||
|
* > > > > > enter pair
|
|||
|
* > > > > > > enter string
|
|||
|
* token quote_, consumed "
|
|||
|
* token string, consumed bar
|
|||
|
* token _quote, consumed "
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; ekzit string
|
|||
|
* token colon, consumed :
|
|||
|
* > > > > > > enter value
|
|||
|
* > > > > > > > enter array
|
|||
|
* token bracket_, consumed [
|
|||
|
* > > > > > > > > enter value
|
|||
|
* token null, consumed null
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; ekzit value
|
|||
|
* > > > > > > > > enter 21
|
|||
|
* > > > > > > > > > enter 20
|
|||
|
* token comma, consumed ,
|
|||
|
* > > > > > > > > > > enter value
|
|||
|
* token number, consumed 42
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; ekzit value
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; ekzit 20
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; ekzit 21
|
|||
|
* token _bracket, consumed ]
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; &lt; ekzit array
|
|||
|
* &lt; &lt; &lt; &lt; &lt; &lt; ekzit value
|
|||
|
* &lt; &lt; &lt; &lt; &lt; ekzit pair
|
|||
|
* &lt; &lt; &lt; &lt; ekzit 12
|
|||
|
* &lt; &lt; &lt; ekzit 13
|
|||
|
* token _brace, consumed }
|
|||
|
* &lt; &lt; ekzit object
|
|||
|
* &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 &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&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>
|