Je travaille lentement pour terminer mon diplôme, et ce semestre est Compilers 101. Nous utilisons le Dragon Book . Peu de temps dans le cours et nous parlons de l'analyse lexicale et de la façon dont elle peut être implémentée via des automates finis déterministes (ci-après, DFA). Configurez vos différents états de lexer, définissez les transitions entre eux, etc.
Mais le professeur et le livre proposent de les implémenter via des tables de transition qui équivalent à un tableau 2D géant (les différents états non terminaux comme une dimension, et les symboles d'entrée possibles comme l'autre) et une instruction switch pour gérer tous les terminaux ainsi que la répartition vers les tables de transition dans un état non terminal.
La théorie est très bien, mais en tant que personne qui a écrit du code pendant des décennies, l'implémentation est vile. Ce n'est pas testable, ce n'est pas maintenable, ce n'est pas lisible, et c'est une douleur et demie à déboguer. Pire encore, je ne vois pas comment ce serait à distance pratique si le langage était compatible UTF. Avoir un million d'entrées de table de transition par état non terminal devient vite imprudent.
Alors, quel est le problème? Pourquoi le livre définitif sur le sujet dit-il de procéder de cette façon?
Le surcoût des appels de fonction est-il vraiment si important? Est-ce quelque chose qui fonctionne bien ou est nécessaire lorsque la grammaire n'est pas connue à l'avance (expressions régulières?)? Ou peut-être quelque chose qui gère tous les cas, même si des solutions plus spécifiques fonctionneront mieux pour des grammaires plus spécifiques?
( Remarque: la duplication possible " Pourquoi utiliser une approche OO au lieu d'une déclaration de commutateur géant? " est proche, mais je ne me soucie pas de OO. Une approche fonctionnelle ou même une approche impérative plus saine avec des fonctions autonomes serait bien.)
Et à titre d'exemple, considérons un langage qui n'a que des identifiants, et ces identifiants le sont [a-zA-Z]+
. Dans l'implémentation DFA, vous obtiendrez quelque chose comme:
private enum State
{
Error = -1,
Start = 0,
IdentifierInProgress = 1,
IdentifierDone = 2
}
private static State[][] transition = new State[][]{
///* Start */ new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
///* IdentifierInProgress */ new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
///* etc. */
};
public static string NextToken(string input, int startIndex)
{
State currentState = State.Start;
int currentIndex = startIndex;
while (currentIndex < input.Length)
{
switch (currentState)
{
case State.Error:
// Whatever, example
throw new NotImplementedException();
case State.IdentifierDone:
return input.Substring(startIndex, currentIndex - startIndex);
default:
currentState = transition[(int)currentState][input[currentIndex]];
currentIndex++;
break;
}
}
return String.Empty;
}
(bien que quelque chose qui gérerait correctement la fin du fichier)
Par rapport à ce que j'attendrais:
public static string NextToken(string input, int startIndex)
{
int currentIndex = startIndex;
while (currentIndex < startIndex && IsLetter(input[currentIndex]))
{
currentIndex++;
}
return input.Substring(startIndex, currentIndex - startIndex);
}
public static bool IsLetter(char c)
{
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}
Le code étant NextToken
refactorisé dans sa propre fonction une fois que vous disposez de plusieurs destinations depuis le début de DFA.