• Nenhum resultado encontrado

O FRAMEWORK DE GRAMÁTICA DE ATRIBUTO

Aspectos interprocedurais da inferência de tipos

4.3 O FRAMEWORK DE GRAMÁTICA DE ATRIBUTO

Um formalismo que tem sido proposto para realizar análise sensível ao contexto é a gramática de atributo, ou gramática livre de contexto atribuída, que consiste em uma gramática livre de contexto aumentada por um conjunto de regras que especificam computações. Cada regra define um valor, ou atributo, em termos dos valores de outros atributos. A regra associa o atributo a um símbolo específico da gramática; cada ocorrência do símbolo em uma árvore de derivação (ou árvore sintática) tem uma ocorrência correspondente do atributo. As regras são funcionais; não implicam uma ordem de avaliação específica, e definem o valor de cada atributo de forma única.

Para tornar estas noções mais concretas, considere uma gramática livre de contexto para números binários com sinal. A Figura 4.4 define a gramática SBN = (T,NT,S,P), que gera todos os números binários com sinal, como –101, +11, –01 e +11111001100, e exclui os números binários sem sinal, como 10.

A partir de SBN, podemos construir uma gramática de atributos que associa Number ao valor do número binário com sinal que ele representa. Para construir uma gramática de atributo a partir de uma gramática livre de contexto, temos que decidir quais atributos cada nó precisa, e elaborar as produções com regras que definem valores para esses atributos. Para nossa versão atribuída de SBN, os seguintes atributos são necessários:

Atributo

Valor ligado a um ou mais dos nós em uma árvore de derivação.

Símbolo Atributos

Number value

Sign negative

List position, value

Bit position, value

Neste caso, nenhum atributo é necessário para os símbolos terminais.

A Figura 4.5 mostra as produções de SBN elaboradas com regras de atribuição. Subscritos são acrescentados aos símbolos da gramática sempre que um símbolo específico aparece várias vezes em uma única produção. Esta prática retira a ambiguidade das referências a este símbolo nas regras. Assim, as duas ocorrências de List na produção 5 têm subscritos, tanto na produção quanto nas regras corres- pondentes.

As regras acrescentam atributos aos nós da árvore de derivação por meio de seus nomes. O atributo mencionado em uma regra deve ser invocado a cada ocorrência desse tipo de nó.

Cada regra especifica o valor de um atributo em termos de constantes literais e os atributos de outros símbolos na produção. Uma regra pode passar informações do lado esquerdo da produção para o seu lado direito; e, também, no sentido inverso. As regras para a produção 4 passam informações nos dois sentidos. A primeira regra

cada nó List. Cada regra define implicitamente um conjunto de dependências; o atributo sendo definido depende de cada argumento para a regra. Tomadas sobre a árvore de derivação inteira, essas dependências formam um grafo de dependência de atributos. As arestas no grafo seguem o fluxo de valores na avaliação de uma regra; uma aresta de nodei.fieldj para nodek.fieldl indica que a regra definindo nodek.fieldl usa o valor de nodei.fieldj como uma de suas entradas. A Figura 4.6b mostra o grafo de dependência de atributo induzido pela árvore de derivação para a string –101. O fluxo bidirecional de valores que antes observamos (por exemplo, na produção 4) aparece no grafo de dependência, onde as setas indicam tanto o fluxo para cima, até a raiz (Number), quanto para baixo, até as folhas. Os nós List mostram este efeito mais claramente. Distinguimos entre os atributos com base no sentido do fluxo de valor. Atributos sintetizados são definidos pelo fluxo de informações de baixo para cima; uma regra que define um atributo para o lado esquerdo da produção cria um atributo sintetizado, que pode retirar valores do próprio nó, dos seus descendentes na árvore de derivação e de constantes. Atributos herdados são definidos pelo fluxo de informações de cima para baixo e lateralmente; a regra que define um atributo para o lado direito da produção cria um atributo herdado. Como a regra de atribuição pode nomear qualquer símbolo usado na produção correspondente, um atributo herdado pode retirar valores do próprio nó, de seu pai e de seus irmãos na árvore de derivação, e de constantes. A

regras é malformada. Dizemos que essas regras são circulares porque podem criar um ciclo no grafo de dependência. Por enquanto, vamos ignorar a circularidade; a Seção 4.3.2 trata desta questão.

O grafo de dependência captura o fluxo de valores que um avaliador precisa respeitar na avaliação de um exemplar de uma árvore atribuída. Se a gramática for não circular, ela impõe uma ordem parcial sobre os atributos. Esta ordem parcial determina quando a regra que define cada atributo pode ser avaliada. A ordem de avaliação não está relacionada à ordem em que as regras aparecem na gramática.

Considere a ordem de avaliação para as regras associadas ao nó List mais alto na árvore — o filho à direita de Number. O nó resulta da aplicação da produção 5, List → List Bit; esta aplicação acrescenta três regras à avaliação. As duas regras que definem atributos herdados para os filhos do nó List precisam ser executadas primeiro. Elas dependem do valor de List.position e definem os atributos position para as subárvores do nó. A terceira regra, que define o atributo value do nó List, não pode ser executada até que as duas subárvores tenham definido os atributos value. Como estas subárvores não podem ser avaliadas até que as duas primeiras regras no nó List o tenham sido, a sequência de avaliação incluirá as duas primeiras regras mais cedo, e a terceira muito mais tarde.

Para criar e usar uma gramática de atributo, o construtor de compiladores determina um conjunto de atributos para cada símbolo da gramática e projeta um conjunto de regras para calcular seus valores. Essas regras especificam uma computação para qualquer ár- vore de derivação válida. Para criar uma implementação, ele precisa criar um avaliador, o que pode ser feito com um programa ad hoc ou usando um gerador de avaliador — a opção mais atraente. O gerador de avaliador toma como entrada a especificação para a gramática de atributo, e produz como saída, o código para um avaliador. Este é o atrativo das gramáticas de atributo para o construtor de compiladores; as ferramentas usam uma especificação de alto nível, não procedimental, e automaticamente produzem uma implementação.

Um detalhe crítico por trás do formalismo da gramática de atributo é a noção de que as regras de atribuição podem ser associadas às produções na gramática livre de contexto. Como as regras são funcionais, os valores que produzem são independentes da ordem de avaliação, para qualquer ordem que respeite os relacionamentos incorporados no grafo de dependência de atributo. Na prática, qualquer ordem que avalia uma regra, somente após todas as suas entradas terem sido definidas respeita as dependências.

4.3.1 Métodos de avaliação

O modelo de gramática de atributo tem uso prático somente se pudermos criar avaliado- res que interpretem as regras para avaliar um exemplar do problema automaticamente Atributo sintetizado

Atributo definido totalmente em termos dos atributos do nó, seus filhos e constantes.

Atributo herdado

Atributo definido totalmente em termos dos atributos próprios do nó e daqueles de seus irmãos ou seu pai na árvore de derivação (além de constantes).

A regra node.field ← 1 pode ser tratada como sintetizada ou herdada.

cíclico.

Figura 4.6b mostra que os atributos value e negative são sintetizados, enquanto position é herdado.

Qualquer esquema para avaliar atributos precisa respeitar os relacionamentos codi- ficados implicitamente no grafo de dependência de atributo. Cada atributo deve ser definido por alguma regra. Se esta regra depender dos valores de outros atributos, não poderá ser avaliada até que todos esses valores tenham sido definidos. Se não depender, então deve produzir seu valor a partir de uma constante ou de alguma fonte externa. Desde que nenhuma regra conte com seu próprio valor, as regras devem definir cada valor de forma única.

Naturalmente, a sintaxe das regras de atribuição permite que uma regra referencie seu próprio resultado, direta ou indiretamente. Uma gramática de atributo contendo tais regras é malformada. Dizemos que essas regras são circulares porque podem criar um ciclo no grafo de dependência. Por enquanto, vamos ignorar a circularidade; a Seção 4.3.2 trata desta questão.

O grafo de dependência captura o fluxo de valores que um avaliador precisa respeitar na avaliação de um exemplar de uma árvore atribuída. Se a gramática for não circular, ela impõe uma ordem parcial sobre os atributos. Esta ordem parcial determina quando a regra que define cada atributo pode ser avaliada. A ordem de avaliação não está relacionada à ordem em que as regras aparecem na gramática.

Considere a ordem de avaliação para as regras associadas ao nó List mais alto na árvore — o filho à direita de Number. O nó resulta da aplicação da produção 5, List → List Bit; esta aplicação acrescenta três regras à avaliação. As duas regras que definem atributos herdados para os filhos do nó List precisam ser executadas primeiro. Elas dependem do valor de List.position e definem os atributos position para as subárvores do nó. A terceira regra, que define o atributo value do nó List, não pode ser executada até que as duas subárvores tenham definido os atributos value. Como estas subárvores não podem ser avaliadas até que as duas primeiras regras no nó List o tenham sido, a sequência de avaliação incluirá as duas primeiras regras mais cedo, e a terceira muito mais tarde.

Para criar e usar uma gramática de atributo, o construtor de compiladores determina um conjunto de atributos para cada símbolo da gramática e projeta um conjunto de regras para calcular seus valores. Essas regras especificam uma computação para qualquer ár- vore de derivação válida. Para criar uma implementação, ele precisa criar um avaliador, o que pode ser feito com um programa ad hoc ou usando um gerador de avaliador — a opção mais atraente. O gerador de avaliador toma como entrada a especificação para a gramática de atributo, e produz como saída, o código para um avaliador. Este é o atrativo das gramáticas de atributo para o construtor de compiladores; as ferramentas usam uma especificação de alto nível, não procedimental, e automaticamente produzem uma implementação.

Um detalhe crítico por trás do formalismo da gramática de atributo é a noção de que as regras de atribuição podem ser associadas às produções na gramática livre de contexto. Como as regras são funcionais, os valores que produzem são independentes da ordem de avaliação, para qualquer ordem que respeite os relacionamentos incorporados no grafo de dependência de atributo. Na prática, qualquer ordem que avalia uma regra, somente após todas as suas entradas terem sido definidas respeita as dependências.

4.3.1 Métodos de avaliação

O modelo de gramática de atributo tem uso prático somente se pudermos criar avaliado- res que interpretem as regras para avaliar um exemplar do problema automaticamente Atributo sintetizado

Atributo definido totalmente em termos dos atributos do nó, seus filhos e constantes.

Atributo herdado

Atributo definido totalmente em termos dos atributos próprios do nó e daqueles de seus irmãos ou seu pai na árvore de derivação (além de constantes).

A regra node.field ← 1 pode ser tratada como sintetizada ou herdada.

Circularidade

Uma gramática de atributo é circular se puder, para algumas entradas, criar um grafo de dependência cíclico.

de atributos, o ordenaria topologicamente e usaria esta ordem para avaliar os atributos.