• Nenhum resultado encontrado

Rastreamento de loads, revisão

Considere, novamente, o problema de rastrear operações load que surgiram como parte da estimativa de custos de execução. A maior parte da complexidade na gramática de atributos para este problema surgiu da necessidade de passar informações pela árvore. Em um esquema de tradução ad hoc dirigida pela sintaxe que usa tabela de símbolos, o problema é fácil de ser tratado. O construtor de compiladores pode reservar um campo na tabela para manter um booleano que indica se esse identificador já foi ou não carregado por um load. O campo é inicialmente definido como false. O código crítico é associado à produção Fator → nome. Se a entrada na tabela de símbolos de nome indicar que ele não foi carregado por um load, então o custo é atualizado e o campo é definido como true.

A Figura 4.12 ilustra este caso, juntamente com todas as outras ações. Como as ações podem conter um código qualquer, o compilador pode acumular cost em uma única variável, ao invés de criar um atributo cost em cada nó da árvore sintática. Este es- quema exige menos ações do que as regras de atribuição para o modelo de execução mais simples, embora forneça a precisão do modelo mais complexo.

Observe que várias produções não possuem ações, e as restantes são simples, exceto para aquela tomada sobre uma redução de nome. Toda complicação introduzida pelo rastreamento de loads encontra-se nesta única ação. Compare isto com a versão da gramática de atributo, onde a tarefa de passar os conjuntos Before e After chegou a dominar a especificação. A versão ad hoc é mais limpa e mais simples, em parte porque o problema se encaixa bem na ordem de avaliação ditada pelas ações de redução (reduce) em um parser shift-reduce. Naturalmente, o construtor de compiladores precisa implementar a tabela de símbolos ou importá-la de alguma biblioteca de implementações de estruturas de dados.

Claramente, algumas dessas estratégias também poderiam ser aplicadas em um frame- work de gramática de atributo. Porém, elas violam a natureza funcional da gramática de atributo. Elas forçam partes críticas do trabalho, do framework de gramática de atributo para a configuração ad hoc.

O esquema na Figura 4.12 ignora uma questão crítica: inicializar cost. A gramática, conforme está escrita, não contém uma produção que possa apropriadamente inicializar cost como zero. A solução, já descrita, é modificar a gramática de um modo que crie

bem ao framework da gramática de atributo, que implicitamente exige a presença de uma árvore sintática. Todas as informações de tipo podem ser ligadas a ocorrências de símbolos da gramática, que correspondem exatamente aos nós na árvore sintática. Podemos reformular este problema em um framework ad hoc, como mostra a Figura 4.13. Ele usa as funções de inferência de tipo introduzidas com a Figura 4.7. O framework resultante parece ser semelhante à gramática de atributo para a mesma finalidade da Figura 4.7. O framework ad hoc não oferece uma vantagem real para este problema.

Criação de uma árvore sintática abstrata

Os front ends de compiladores precisam criar uma representação intermediária do programa para usar na parte do meio do compilador e no seu back end. As árvores sintáticas abstratas são uma forma comum de IR estruturada em árvore. A tarefa de criar uma AST assenta-se bem a um esquema de tradução ad hoc dirigida pela sintaxe. Suponha que o compilador tenha uma série de rotinas chamadas MakeNodei, para 0 ≤ i ≤ 3. A rotina usa, como primeiro argumento, uma constante que identifica exclusi- vamente o símbolo da gramática que o novo nó representará. Os i argumentos restantes

são os nós que encabeçam cada uma das i subárvores. Assim, MakeNode0 (number)

constrói um nó folha e o marca como representando um num. De modo semelhante,

cria uma AST cuja raiz é um nó para plus com dois filhos, cada um deles um nó folha para num.

Para criar uma árvore sintática abstrata, o esquema de tradução ad hoc dirigida pela sintaxe segue dois princípios gerais:

1. Para um operador, cria um nó com um filho para cada operando. Assim, 2 + 3

cria um nó binário para + com os nós para 2 e 3 como filhos.

2. Para uma produção inútil, como Termo → Fator, reutiliza o resultado da ação Fator como seu próprio resultado.

As rotinas MakeNode podem implementar a árvore de qualquer maneira que seja apropriada. Por exemplo, podem mapear a estrutura para uma árvore binária, conforme discutido na Seção B.3.1.

remover o máximo desses detalhes (e adiar algumas questões mais profundas para os capítulos seguintes), o framework de exemplo utiliza quatro rotinas de suporte.

1. Address utiliza um nome de variável como seu argumento. Ela retorna o número

de um registrador que contém o valor especificado por nome. Se for preciso, gera código para carregar este valor.

2. Emit trata dos detalhes da criação de uma representação concreta para as diversas

operações ILOC, e pode formatá-las e imprimi-las em um arquivo. Como alternativa, pode ainda criar uma representação interna, para uso posterior.

3. NextRegister retorna um novo número de registrador. Uma implementação sim-

ples poderia incrementar um contador global.

4. Value utiliza um número como seu argumento e retorna um número de regis-

trador. Garante que o registrador contém o número passado como seu argumento, e, se for preciso, gera código para mover esse número para o registrador.

A Figura 4.15 mostra o framework dirigido pela sintaxe para este problema. As ações se comunicam passando os nomes de registradores na pilha de análise. As ações passam esses nomes para Emit conforme a necessidade, a fim de criar as operações que im- plementam a expressão de entrada.