Gramáticas de Atributos
Last updated
Last updated
Já vimos que atribuir sentido ao código fonte de uma linguagem requer, não só, correcção sintáctica (assegurada por gramáticas independentes de contexto) como também correcção semântica.
Nesse sentido, é de toda a conveniência ter acesso a toda a informação gerada pela análise sintáctica, i.e. à árvore sintáctica, e poder associar nova informação aos respectivos nós.
Este é o objectivo da gramática de atributos:
Cada símbolo da gramática da linguagem (terminal ou não terminal) pode ter a si associado um conjunto de zero ao mais atributos.
Um atributo pode ser um número, uma palavra, um tipo, ...
O cálculo de cada atributo tem de ser feito tendo em consideração a dependência da informação necessária para o seu valor.
Entre os diferentes tipos de atributos, existem alguns cujo valor depende apenas da sua vizinhança sintáctica.
Um desses exemplos é o valor de uma expressão aritmética (que para além disso, depende apenas do próprio nó e, eventualmente, de nós descendentes).
Existem também atributos que (podem) depender de informação remota.
É o caso, por exemplo, do tipo de dados de uma expressão que envolva a utilização de uma variável ou invocação de um método.
Os atributos podem ser classificados duas formas, consoante as dependências que lhes são aplicáveis:
Dizem-se sintetizados, se o seu valor depende apenas de nós descendentes (i.e. se o seu valor depende apenas dos símbolos existentes no respectivo corpo da produção).
Dizem-se herdados, se depende de nós "irmãos" ou de nós ascendentes.
Formalmente podem-se designar os atributos anotando com uma seta no sentido da dependência (para cima, nos atributos sintetizados; e para baixo nos herdados).
Considere a seguinte gramática:
E -> P + E | P P -> T * P | T T -> (E) | int
Se quisermos definir um atributo v para o valor da expressão, temos um exemplo de um atributo sintetizado.
Por exemplo, para a entrada — 1 + 2 ∗ 3 — temos a seguinte árvore sintáctica anotada:
E1 -> P + E2
E1y = Py + E2.v
E -> P
E.v = P.v
P1 -> T * P2
P1.v = T.v * P2.v
P -> T
P.v = T.v
T -> (E)
T.v = E.v
T -> int
T.v = int.value
Considere a seguinte gramática:
D -> TL T -> int | real L -> id L | id
Se quisermos definir um atributo t para indicar o tipo de cada variável id, temos um exemplo de um atributo herdado.
Por exemplo, para a entrada — int a, b — temos a seguinte árvore sintáctica anotada:
D -> TL
D.t = T.t L.t = T.t
T -> int
T.t = int
T -> real
T.t = real
L1 -> id, L2
id.t = L1.t L2.t = L1.t
L -> id
id.t = L.t
Podemos declarar atributos de formas distintas:
Directamente na gramática independente de contexto recorrendo a argumentos e resultados de regras sintáctica.
Indirectamente fazendo uso do array associativo ParseTreeProperty:
Podemos ainda utilizar o resultados dos métodos visit.
Este array tem como chave nós da árvore sintáctica, e permite simular quer argumentos, quer resultados, de regras.
A diferença está nos locais onde o seu valor é atribuído e acedido.
Para simular a passagem de argumentos basta atribuir-lhe o valor antes de percorrer o respectivo nó (nos listeners usualmente nos métodos enter...), sendo o acesso feito no próprio nó.
Para simular resultados, faz-se como no exemplo dado (i.e. atribui-se o valor no próprio nó, e acede-se nos nós ascendentes).
Podemos associar três tipos de informação a regras sintácticas:
Informação com origem em regras utilizadas no corpo da regra (atributos sintetizados);
Informação com origem em regras que utilizam esta regra no seu corpo (atributos herdados);
Informação local à regra.
Em ANTLR4 a utilização directa de todos estes tipos de atributos é muito simples e intuitiva:
Atributos sintetizados: resultado de regras;
Atributos herdados: argumentos de regras;
Atributos locais.
Alternativamente, podemos utilizar o array associativo ParseTreeProperty (que se justifica apenas para as duas primeiras, já que para a terceira podemos utilizar variáveis locais ao método respectivo); ou o resultado dos métodos visit (no caso de se utilizar visitors) para atributos sintetizados.