Estou lentamente trabalhando para terminar minha graduação, e este semestre é o Compilers 101. Estamos usando o Dragon Book . Logo no início do curso, estamos falando sobre análise lexical e como ela pode ser implementada por meio de autômatos finitos determinísticos (doravante, DFA). Configure seus vários estados de lexer, defina transições entre eles, etc.
Mas tanto o professor quanto o livro propõem implementá-los por meio de tabelas de transição que equivalem a uma matriz 2d gigante (os vários estados não terminais como uma dimensão e os possíveis símbolos de entrada como a outra) e uma instrução switch para lidar com todos os terminais bem como despachar para as tabelas de transição se estiver em um estado não terminal.
A teoria está muito bem, mas como alguém que realmente escreveu código por décadas, a implementação é vil. Não é testável, não é sustentável, não é legível e é uma dor e meia para depurar. Pior ainda, não consigo ver como seria remotamente prático se o idioma fosse compatível com UTF. Ter um milhão ou mais de entradas na tabela de transição por estado não terminal fica improdutivo às pressas.
Então, qual é o problema? Por que o livro definitivo sobre o assunto está dizendo para fazê-lo dessa maneira?
A sobrecarga das chamadas de função é realmente assim? Isso é algo que funciona bem ou é necessário quando a gramática não é conhecida antecipadamente (expressões regulares?)? Ou talvez algo que lide com todos os casos, mesmo que soluções mais específicas funcionem melhor para gramáticas mais específicas?
( observação: possível duplicata " Por que usar uma abordagem OO em vez de uma declaração de switch gigante? " está próxima, mas eu não me importo com OO. Uma abordagem funcional ou mesmo uma abordagem mais sadia e imperativa com funções independentes seria adequada.)
E, por exemplo, considere uma linguagem que tenha apenas identificadores, e esses identificadores são [a-zA-Z]+
. Na implementação do DFA, você obteria algo como:
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;
}
(embora algo que lide com o final do arquivo corretamente)
Comparado com o que eu esperaria:
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'));
}
Com o código NextToken
refatorado para sua própria função, quando você tiver vários destinos desde o início do DFA.