Quais são os bons recursos para escrever um lexer em C ++ (livros, tutoriais, documentos), quais são algumas boas técnicas e práticas?
Eu olhei na internet e todo mundo diz para usar um gerador lexer como lex. Não quero fazer isso, quero escrever um lexer à mão.
Respostas:
Lembre-se de que toda máquina de estados finitos corresponde a uma expressão regular, que corresponde a um programa estruturado usando
if
ewhile
instruções.Então, por exemplo, para reconhecer números inteiros, você poderia ter a máquina de estado:
ou a expressão regular:
ou o código estruturado:
Pessoalmente, eu sempre escrevo lexers usando o último, porque IMHO não é menos claro e não há nada mais rápido.
fonte
*pc
, certo? Likewhile(isdigit(*pc)) { value += pc; pc++; }
. Depois que}
você converter o valor em um número e atribuí-lo a um token.n = n * 10 + (*pc++ - '0');
. Torna-se um pouco mais complexo para ponto flutuante e notação 'e', mas não é ruim. Tenho certeza de que poderia salvar um pouco de código colocando os caracteres em um buffer e ligandoatof
ou o que for. Não seria mais rápido.Lexers são máquinas de estado finito. Portanto, eles podem ser construídos por qualquer biblioteca FSM de uso geral. Para os propósitos de minha própria educação, no entanto, escrevi minha própria, usando modelos de expressão. Aqui está o meu lexer:
É apoiado por uma biblioteca de máquinas de estado finito, com rastreamento de retorno e com base em iterador, com aproximadamente 400 linhas de comprimento. No entanto, é fácil ver que tudo o que eu tinha a fazer era construção simples operações booleanas, como
and
,or
, enot
, e um par de operadores de estilo regex como*
para-ou-mais zero,eps
que significa "match qualquer coisa" eopt
que significa "jogo qualquer coisa, mas não consuma ". A biblioteca é totalmente genérica e baseada em iteradores. O material MakeEquality é um teste simples de igualdade entre*it
e o valor passado, e MakeRange é um<= >=
teste simples .Eventualmente, estou planejando mudar de retorno para previsão.
fonte
MakeEquality
snippet? Especificamente, o objeto retornado por essa função. Parece muito interessante.Primeiro de tudo, há coisas diferentes acontecendo aqui:
Geralmente, esperamos que um lexer execute as três etapas de uma só vez, no entanto, o último é inerentemente mais difícil e há alguns problemas com a automação (mais sobre isso mais tarde).
O lexer mais incrível que eu conheço é o Boost.Spirit.Qi . Ele usa modelos de expressão para gerar suas expressões lexer e, uma vez acostumado à sua sintaxe, o código parece realmente legal. Embora seja compilado muito lentamente (modelos pesados), é melhor isolar as várias partes em arquivos dedicados para evitar recompilá-las quando não foram tocadas.
Existem algumas armadilhas no desempenho, e o autor do compilador Epoch explica como ele conseguiu uma velocidade de 1000x por meio de criação de perfil e investigação intensivos sobre como o Qi funciona em um artigo .
Por fim, também são gerados códigos por ferramentas externas (Yacc, Bison, ...).
Mas prometi um artigo sobre o que havia de errado em automatizar a verificação gramatical.
Se você verificar o Clang, por exemplo, perceberá que, em vez de usar um analisador gerado e algo como Boost.Spirit, eles pretendem validar a gramática manualmente usando uma técnica genérica de Análise de Descida. Certamente isso parece atrasado?
De fato, há uma razão muito simples: recuperação de erros .
O exemplo típico, em C ++:
Percebeu o erro? Um ponto e vírgula ausente logo após a declaração de
Foo
.É um erro comum, e Clang se recupera perfeitamente ao perceber que está simplesmente ausente e
void
não é uma instância,Foo
mas parte da próxima declaração. Isso evita o diagnóstico de mensagens de erro enigmáticas.A maioria das ferramentas automatizadas não possui maneiras (pelo menos óbvias) de especificar esses erros prováveis e como se recuperar deles. Muitas vezes, a recuperação requer um pouco de análise sintática, por isso está longe de ser evidente.
Portanto, há uma troca envolvida no uso de uma ferramenta automatizada: você obtém seu analisador rapidamente, mas é menos amigável.
fonte
Como você quer aprender como os lexers funcionam, presumo que você realmente queira saber como os geradores de lexer funcionam.
Um gerador de lexer pega uma especificação lexical, que é uma lista de regras (pares de expressão regular-token) e gera um lexer. Esse lexer resultante pode transformar uma string de entrada (caractere) em uma string de token de acordo com esta lista de regras.
O método mais comumente usado consiste principalmente em transformar uma expressão regular em um autômato finito determinístico (DFA) por meio de um autômato não determinístico (NFA), além de alguns detalhes.
Um guia detalhado de como fazer essa transformação pode ser encontrado aqui . Observe que eu não li, mas parece muito bom. Além disso, praticamente qualquer livro sobre construção de compilador apresentará essa transformação nos primeiros capítulos.
Se você estiver interessado em slides de palestras de cursos sobre o tema, não há dúvida de que uma quantidade infinita deles é de cursos sobre construção de compiladores. Da minha universidade, você pode encontrar esses slides aqui e aqui .
Existem poucas outras coisas que não são comumente empregadas em lexers ou tratadas em textos, mas são bastante úteis, no entanto:
Em primeiro lugar, o manuseio do Unicode não é trivial. O problema é que a entrada ASCII tem apenas 8 bits de largura, o que significa que você pode facilmente ter uma tabela de transição para todos os estados do DFA, porque eles possuem apenas 256 entradas. No entanto, o Unicode, com 16 bits de largura (se você usar UTF-16), requer tabelas de 64k para cada entrada no DFA. Se você possui gramáticas complexas, isso pode começar a ocupar bastante espaço. O preenchimento dessas tabelas também começa a demorar um pouco.
Como alternativa, você pode gerar árvores de intervalo. Uma árvore de intervalo pode conter as tuplas ('a', 'z'), ('A', 'Z'), por exemplo, que são muito mais eficientes em termos de memória do que a tabela completa. Se você mantiver intervalos sem sobreposição, poderá usar qualquer árvore binária balanceada para esse fim. O tempo de execução é linear no número de bits que você precisa para cada caractere; portanto, O (16) no caso Unicode. No entanto, na melhor das hipóteses, geralmente será um pouco menos.
Mais uma questão é que os lexers, como geralmente gerados, têm realmente um desempenho quadrático no pior dos casos. Embora esse comportamento de pior caso não seja comumente visto, ele pode te morder. Se você se deparar com o problema e quiser resolvê-lo, um documento descrevendo como obter tempo linear pode ser encontrado aqui .
Provavelmente, você poderá descrever expressões regulares na forma de string, como normalmente aparecem. No entanto, analisar essas descrições de expressões regulares nos NFAs (ou possivelmente uma estrutura intermediária recursiva primeiro) é um pouco problemático. Para analisar descrições de expressões regulares, o algoritmo Shunting Yard é muito adequado. A Wikipedia parece ter uma página extensa sobre o algoritmo .
fonte