Gramáticas de Atributos

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.

Dependência local: classificação de atributos

Os atributos podem ser classificados duas formas, consoante as dependências que lhes são aplicáveis:

  1. 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).

  2. 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).

Exemplo dependência local: expressão aritmética

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:

ProduçãoRegra Semântica

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

Exemplo dependência local: declaração

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:

ProduçãoRegra semântica

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

Declaração de atributos associados à árvore sintáctica

Podemos declarar atributos de formas distintas:

  • Directamente na gramática independente de contexto recorrendo a argumentos e resultados de regras sintáctica.

expr[String type] returns[int value]: // type not used
    e1 = expr ’+’ e2=expr
    {$value = $e1.value + $e2.value; }    #Add
    | INT
        {$value = Integer.parseInt($INT.text);}    #Int
    ;
  • Indirectamente fazendo uso do array associativo ParseTreeProperty:

protected ParseTreeProperty<Integer> value=
    new ParseTreeProperty<>();
    
@Override public void exitInt(ExprParser.IntContext ctx){
    value.put(ctx , Integer.parseInt(ctx.INT().getText()));
}

@Override public void exitAdd(ExprParser.AddContext ctx){
    int left = value.get(ctx.e1);
    int right = value.get(ctx.e2);
    value.put(ctx, left+right);
}

Podemos ainda utilizar o resultados dos métodos visit.

Declaração de atributos ParseTreeProperty

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.

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).

Gramáticas de atributos em ANTLR4: síntese

Podemos associar três tipos de informação a regras sintácticas:

  1. Informação com origem em regras utilizadas no corpo da regra (atributos sintetizados);

  2. Informação com origem em regras que utilizam esta regra no seu corpo (atributos herdados);

  3. Informação local à regra.

Em ANTLR4 a utilização directa de todos estes tipos de atributos é muito simples e intuitiva:

  1. Atributos sintetizados: resultado de regras;

  2. Atributos herdados: argumentos de regras;

  3. 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.

Last updated