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 .