• Nenhum resultado encontrado

O exemplo que estamos usando, r[0…9]+, divide o alfabeto de caracteres de entrada

em exatamente quatro classes. Um r cai na classe Registrador. Os dígitos 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9 caem na classe Dígito, o caractere especial retornado quando NextChar esgota sua entrada cai na classe EndOfFile, e qualquer outra coisa cai na classe Outro.

O scanner pode, de modo fácil e eficiente, classificar um determinado caractere, como mostra a Figura 2.16. O estado s0 usa um teste direto sobre ‘r’ para determinar se

char está em Registrador. Como todas as outras classes possuem ações equivalentes no DFA, o scanner não precisa realizar outros testes. Os estados s1 e s2 classificam

char como Dígito entre outras denominações, e aproveitam o fato de que os dígitos de 0 a 9 ocupam posições adjacentes na sequência de classificação ASCII, corres- pondentes aos inteiros de 48 a 57.

Em um scanner no qual a classificação de caractere é mais complicada, o método de tabela de tradução usado no scanner controlado por tabela pode ser menos dispendioso do que testar os caracteres diretamente. Em particular, se uma classe contém múltiplos caracteres que não ocupam posições adjacentes na sequência de classificação, uma pesquisa em tabela pode ser mais eficiente do que o teste direto. Por exemplo, uma classe que contenha os operadores aritméticos +, –, *, \, e ^ (43, 45, 42, 48 e 94 na sequência ASCII) exigiria uma série moderadamente longa de comparações. O uso de uma tabela de tradução, como CharCat no exemplo controlado por tabela, poderia ser mais rápido do que as comparações se a tabela de tradução permanecer no cache primário do processador.

2.5.3 Scanners codificados à mão

Scanners gerados, sejam controlados por tabela ou codificados diretamente, utilizam uma quantidade de tempo pequena e constante por caractere. Apesar disto, muitos compiladores usam scanners codificados à mão. Em um estudo informal de grupos de compiladores comerciais, descobrimos que uma fração surpreendentemente grande os usava. De modo semelhante, muitos dos compiladores open-source populares contam com este tipo de scanner. Por exemplo, o gerador de scanner flex foi aparentemente criado para dar suporte ao projeto gcc, mas o gcc 4.0 usa scanners codificados à mão em vários de seus front ends.

O scanner codificado diretamente reduziu o overhead da simulação do DFA; o codi- ficado à mão pode reduzir o overhead das interfaces entre ele e o restante do sistema. Em particular, uma implementação cuidadosa pode melhorar os mecanismos usados O código no estilo da Figura 2.16 normalmente é

chamado código espaguete, em homenagem ao seu fluxo de controle confuso.

Sequência de classificação

“Ordem alfabética” dos caracteres em um alfabeto, determinada pelos inteiros atribuídos a cada caractere.

ser realizadas em linha; normalmente são codificadas em uma macro, para evitar encher o código com incrementos de ponteiro e recuperação de dados apontados por ponteiro (dereference).

O custo da leitura de um buffer cheio de caracteres tem dois componentes, um overhead fixo grande e um custo por caractere pequeno. Um esquema de buffer e ponteiro amortiza os custos fixos da leitura por muitas buscas de único caractere. Aumentar o buffer reduz o número de vezes que o scanner contrai este custo e reduz o overhead por caractere.

O uso de buffer e ponteiro também ocasiona uma implementação simples e eficaz da operação RollBack que ocorre ao final de ambos os scanners gerados. Para reverter a entrada, o scanner pode simplesmente decrementar o ponteiro de entrada. Esse es- quema funciona desde que o scanner não decremente o ponteiro além do início do buffer. Nesse ponto, porém, o scanner precisa de acesso ao conteúdo anterior do buffer. Na prática, o construtor de compiladores pode limitar a distância de rollback que um scanner precisará. Com o rollback limitado, o scanner pode simplesmente usar dois buffers adjacentes e incrementar o ponteiro em um padrão de módulo, como vemos a seguir:

Para ler um caractere, o scanner incrementa o ponteiro, módulo 2n, e retorna o caractere nesse local. Para reverter um caractere, o programa decrementa o ponteiro de en- trada, módulo 2n. E também precisa gerenciar o conteúdo do buffer, lendo caracteres adicionais do fluxo de entrada conforme a necessidade.

Tanto NextChar quanto RollBack possuem implementações simples e eficientes, como mostra a Figura 2.17. Cada execução de NextChar carrega um caractere, incrementa o ponteiro Input e testa se preenche ou não o buffer. A cada n ca- racteres, ele preenche o buffer. O código é pequeno o suficiente para ser incluído em linha, talvez gerado a partir de uma macro. Este esquema amortiza o custo de preenchimento do buffer sobre n caracteres. Escolhendo um tamanho razoável para n, como 2048, 4096, ou mais, o construtor de compiladores pode manter baixo o overhead de E/S.

RollBack é ainda menos dispendioso. Ele realiza um teste para garantir que o conteúdo do buffer é válido e depois decrementa o ponteiro de entrada. Novamente, a

Buffering duplo

O esquema que usa dois buffers de entrada em um padrão de módulo para fornecer rollback limitado normalmente é chamado buffering duplo.

implementação é suficientemente simples para ser expandida em linha. (Se usássemos essa implementação de NextChar e RollBack nos scanners gerados, RollBack precisaria truncar o caractere final para fora do lexema.)

Como uma consequência natural do uso de buffers finitos, RollBack tem um histórico limitado no fluxo de entrada. Para evitar que decremente o ponteiro além do início desse contexto, NextChar e RollBack cooperam. O ponteiro Fence sempre indica o início do contexto válido. NextChar define Fence toda vez que preenche um buffer. RollBack verifica Fence toda vez que tenta decrementar o ponteiro Input.

Depois de uma longa série de operações NextChar, digamos, mais de n vezes, RollBack sempre pode reverter pelo menos n caracteres. Porém, uma sequência de chamadas para NextChar e RollBack que funciona para a frente e para trás no buffer pode criar uma situação em que a distância entre Input e Fence é menor que n. Valores maiores de n diminuem esta probabilidade. As distâncias de reversão esperadas devem ser levadas em consideração na seleção do tamanho do buffer, n.