Le compilateur de la version 2.1.7

Avec dotclear 2.1, le moteur de templates est diablement court en terme de code, et pas moins efficace. Son fonctionnement est le suivant :

  1. Le moteur récupère la liste des blocs déclarés
  2. Pour chaque bloc déclaré, il recherche <tpl:Nomdubloc>...</tpl:Nomdubloc> (j'ai volontairement omis les attributs, pour simplifier)
  3. A chaque bloc trouvé, remplace ce bloc par ce que retourne le traitement de ce bloc (qui a été enregistré via $core->tpl->addBlock)
  4. Le moteur récupère la liste des valeurs déclarées
  5. Pour chaque bloc déclaré, il recherche {{tpl:NomdeLavaleur}}
  6. A chaque valeur trouvée, il appelle le traitement correspondant à la valeur

Techniquement, les recherches/appels s'effectuent via la méthode PHP preg_replace_callback. Les expressions régulières utilisées sont :

$this->tag_block = '<tpl:(%s)(?:(\s+.*?)>|>)(.*?)</tpl:%s>';
$this->tag_value = '{{tpl:(%s)(\s(.*?))?}}';

(les %s sont remplacés par le nom du bloc/tag en cours)

Avantages du compilateur v2.1.7

  • il a été grandement éprouvé, et fonctionne a priori partout

Inconvénients

  • Les performances sont inversement proportionnelles au nombre de blocs/valeurs déclarés. Chaque nouveau bloc entraînera un appel à preg_replace_callback supplémentaire
  • Il n'est pas possible d'imbriquer 2 blocs ayant le même nom
  • un appel de gestion d'un bloc ne garantit pas que le contenu de ce bloc est du code compilé. Un traitement de bloc peut très bien recevoir comme contenu des <tpl:Block>...</tpl:Block> ou {{tpl:Value}}
  • les blocs/valeurs inconnus ne sont pas détectés, ils sont laissée tels quels dans le code généré

Le compilateur de la version 2.2beta1

L'évolution apportée dans le compilateur de la 2.2beta1 consiste principalement à optimiser le compilateur précédent, et à permettre l'imbrication des balises homonymes.

Son fonctionnement est le suivant :

  1. Le moteur cherche dans le template une chaîne du type <tpl:'quelquechose'>[...]</tpl:'quelquechose'>, en imposant que [...] ne contienne ni <tpl:quelquechose>, ni </tpl:quelquechose>. En clair, si 2 tags homonymes sont imbriqués, on choisit le plus imbriqué
  2. Pour chaque bloc trouvé, s'il a été enregistré on remplace le bloc par le traitement correspondant
  3. tant qu'on a fait au moins un remplacement, on réitère en 1.
  4. Le moteur cherche dans le template une chaîne du type {{tpl:'quelquechose'}}
  5. Pour chaque texte trouvé, si la valeur correspondante a été trouvée, on appelle le traitement correspondant

Techniquement, les recherches/appels s'effectuent toujours via preg_replace_callback. Les expressions régulières utilisées sont :

$this->tag_block = '<tpl:(\w+)(?:(\s+.*?)>|>)((?:[^<]|<(?!/?tpl:\1)|(?R))*)</tpl:\1>';
$this->tag_value = '{{tpl:(\w+)(\s(.*?))?}}';

Attention au mal de crâne : la regexp utilise une structure récursive (?R). Une autre illustration de ces structures (dont s'est fortement inspiré celle de doclear) est montrée dans la documentation PHP (exemple 3)

Avantages du compilateur v2.2beta1

  • Support des balises homonymes imbriquées
  • Gain de performances élevé (environ 3 fois plus rapide que la version précédente)
  • On peut détecter les blocs/valeurs inconnus

Inconvénients

  • un appel de gestion d'un bloc ne garantit pas que le contenu de ce bloc est du code compilé. Un traitement de bloc peut très bien recevoir comme contenu des <tpl:Block>...</tpl:Block> ou tpl:Value
  • on ne peut pas détecter certaines erreurs, comme un bloc non fermé. Le code correspondant sera alors laissé intact
  • et l'inconvénient majeur : la regexp utilisée entraîne une forte consommation de mémoire sur la pile par le PCRE. Les appels aux traitements de blocs/valeurs s'effectuant au sein de l'appel à preg_replace_callback, il peut arriver fréquemment des erreurs de dépassement de pile. Cela se traduit dans le meilleur des cas par un comportement bizarre, et dans le pire des cas par une erreur 500, voire d'un crash d'apache.

Le nouveau compilateur

La situation laissée par le compilateur de la 2.2beta1 n'est pas acceptable en l'état. Même s'il est possible de contourner le problème dans certains cas, cela laisse sur la touche certains utilisateurs.

Avant de parler du nouveau compilateur, un petit retour aux fondements de la compilation. Les 2 "compilateurs" précédemment cités ne sont pas de vrais compilateurs au sens puriste du terme. Ce sont plutôt de gros moteurs de recherche/remplacement. Lorsqu'on parle de compilateur, on évoque en général plusieurs phases :

  1. L'analyse lexicale, qui consiste à transformer le code en entrée en séquence de jetons (token). c'est en général pris en charge par un Lexer (exemple d'outils : lex/flex)
  2. L'analyse syntaxique, qui consiste à vérifier l'enchaînement des jetons à partir d'une grammaire, et à générer une structure d'arbre interprétant le code en entrée (exemple d'outils : yacc/bison)
  3. L'analyse sémantique, qui enrichit l'arbre précédent d'informations sémantiques (ex: tables de symboles, vérifications de typage, ...)
  4. la génération du code, en parcourant l'arbre enrichi.

(J'ai volontairement simplifié les étapes, il y a d'autres étapes intermédiaires dans un processus complet)

Malheureusement, au niveau de PHP, on est assez mal loti côté outils qui faciliteraient notre compilation. Par ailleurs, la grammaire des templates dotclear est relativement basique, on a des blocs, des valeurs, et du texte... pas besoin d'avoir ces 3 étapes, donc. En revanche, elles vont grandement servir d'inspiration. N'oublions pas non plus qu'il n'est pas souhaité faire une usine à gaz, les performances doivent rester correctes.

Le nouveau compilateur fonctionne donc de la manière suivante :

  1. Découpage lexical du template. A ce niveau, on profite de preg_split pour saucissonner le fichier. on récupère ainsi un tableau contenant des "tokens" qui sont soit du texte, soit un tag d'ouverture ou de fermeture de bloc, soit un tag de valeur.
  2. On parcourt la liste des tokens, en construisant progressivement un arbre php (une structure "classique" d'arbre objet a été définie pour l'occasion)
  3. On demande à l'arbre de se compiler. Si c'est une liste de tokens ou un bloc, on retourne la concaténation des compilation des enfants

Techniquement, le preg_split utilisé est le suivant :

$blocks = preg_split(
	'#(<tpl:\w+[^>]*>)|(</tpl:\w+>)|({{tpl:\w+[^}]*}})#msu',$fc,-1,
	PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);

Avantages du nouveau compilateur

  • on se rapproche plus d'une vraie compilation
  • on a un contrôle poussé de la grammaire : il est possible de trouver des blocs/valeurs inexistants, et de contrôler que les blocs sont correctement fermés
  • on a la garantie que quand on appelle un bloc, son contenu a été compilé auparavant.
  • les performances sont moindres que le compilateur 2.2beta1, mais on note toutefois un gain de performances de l'ordre de 15%
  • les regexp utilisées sont plus légères que le compilateur 2.2beta1. De plus, les blocs et valeurs sont traités en dehors du moteur PCRE.

Inconvénients

  • le compilateur est tout frais, et même si plusieurs ont validé son bon fonctionnement, il peut rester quelques défauts
  • ce compilateur consomme un peu plus de mémoire (heap) que les précédents.

Reste à valider que tout fonctionne correctement avec ce nouveau compilateur :)