Gramáticas LL(k)

Análise Sintática Descendente Determinista.

Introdução

A Análise Sintática por pesquisa geral não é eficiente, mesmo após a transformação para a FNG porque continua a ser possível encontrarem-se várias produções para expandir um vértice.

Na situação acima o não determinismo resulta as possíveis múltiplas expansões quando se consideram as variáveis em isolamento. Por exemplo, dada a GIC abaixo, o vértice tem dois filhos possíveis: e .

Mas uma observação mais cuidadosa da pesquisa, considerando o primeiro símbolo do sufixo que falta derivar mostra uma situação interessante:

Pesquisa Descendente de com Avanço
Exemplo de Pesquisa Descendente com Avanço
Em cada vértice podam-se os ramos "incompatíveis".

Neste exemplo, olhando para o próximo terminal durante a pesquisa por obtém-se uma pesquisa determinista.

Nesta secção exploram-se pesquisas deterministas com o auxílio dos "símbolos seguintes".

Conteúdo

A apresentação intuitiva acima precisa de uma representação mais formal. Por exemplo, a derivação de pode ser obtida pela seguinte tabela:

Em cada linha:

  • Há uma variável ativa, que inicialmente é .
  • É escolhida a única produção cujo primeiro símbolo coincide com o símbolo de avanço.
  • O avanço é "transferido" para o prefixo processado e a variável ativa é a primeira que ocorre na produção escolhida na linha anterior.

Interessa formalizar esta exploração com vista a definir métodos rigorosos para:

  1. Determinar se uma GIC é adequada, ou não, a este processo.
  2. Definir algoritmos eficientes baseados na pesquisa com avanço.

Começando pela definição de gramática "adequada" à pesquisa com avanço:

Gramática LL(1). A GIC com terminador é LL(1) se dadas duas derivações esquerdas em que e então .

N.B. "LL" significa "Left-to-right Leftmost derivation". Em português: "derivação esquerda da esquerda-para-a-direita". Note-se que "derivação esquerda" especifica qual é a variável a tratar enquanto que "da esquerda-para-a-direita" indica que a palavra é processada sequencialmente do primeiro símbolo para o último.

N.B. O terminador ocorre exatamente uma vez nas palavras geradas e é sempre o último símbolo. A sua função é garantir que há sempre um símbolo de avanço na palavra analisada. Fica como exercício encontrar um algoritmo que transforma uma GIC qualquer, , noutra, com terminador , de forma que .

Intuitivamente a definição de GIC LL(1) diz que não há duas produções distintas de que produzem sufixos terminais que começam pelo mesmo terminal. Ou seja, os resultados finais da aplicação de duas produções de distintas difere logo no primeiro símbolo.

As gramáticas LL(1) têm algumas propriedades interessantes:

Propriedades das Gramáticas LL(1). Seja uma GIC:

  1. Se é LL(1) então não é ambígua,
  2. Se alguma variável de for recursiva à esquerda então não é LL(1).

A generalização de LL(1) para mais do que um símbolo de avanço é representada por . Este caso é pouco interessante em termos teóricos porque torna a notação mais críptica sem progredir na resolução da análise sintática.


Note-se que uma gramática na FNG quase que é LL(1). O problema está na possibilidade de várias produções começarem pelo mesmo terminal. Para ajudar a ultrapassar esta situação é preciso "arrumar" as produções que começam pelo mesmo símbolo.

Fatorização à Esquerda. Seja uma GIC. Supondo que as produções de são

em que então a GIC obtida de

  1. Acrescentando uma nova variável, .
  2. Substituindo as produções de por .
  3. Acrescentado as produções .

é equivalente a .

Com a fatorização as várias produções de que começam pelo mesmo prefixo, ficam agrupadas numa só produção, e a nova variável, , gera os restantes sufixos.

Por exemplo, recuperando a gramática que ilustrou da construção da FNG:

A fatorização pode ser aplicada repetidas vezes até que o resultado seja adequado, por exemplo uma GIC LL(1).


Para determinar se uma GIC é LL(1) a partir da definição pode ser confuso. Para ajudar neste problema mas também para definir um algoritmo determinista de análise sintática para gramáticas LL(1) usam-se os primeiros, seguintes e os diretores.

Primeiros. Seguintes. Seja uma GIC.

Os primeiros de são os terminais que ocorrem na primeira posição das palavras derivadas de :

Os seguintes de são os terminais que ocorrem imediatamente a seguir a nalguma derivação de :

Por exemplo, para a GIC

O conjunto dos ...... é ...
primeiros de
primeiros de
seguintes de

A partir da definição não é simples calcular os conjuntos dos primeiros e dos seguintes. Para esse cálculo há dois algoritmos gráficos:

Grafo dos Primeiros. Seja uma GIC. O grafo dos primeiros é um grafo em que os vértices são os símbolos de e para cada produção :

  1. Acrescenta-se a aresta .
  2. Se , acrescenta-se a aresta .
  3. Assim sucessivamente até se esgotarem os ou .

O grafo dos primeiros tem um caminho se e só se .

Continuando com a GIC anterior, obtém-se

Exemplo de Grafo dos Primeiros
Exemplo de Grafo dos Primeiros
N.B. Os "cantos" das arestas são arredondados.

e, portanto, os primeiros de cada variável são:

ou simplificando a notação:

Este método mostra apenas os primeiros das variáveis. Para as restantes palavras:

Em geral, calculam-se recursivamente os :

  • .
  • Para .
  • Para usa-se o grafo dos primeiros.
  • Para :

Depois dos primeiros (das variáveis) podem calcular-se os seguintes.

Grafo dos Seguintes. Seja uma GIC. O grafo dos seguintes é um grafo em que os vértices são os símbolos de e para cada produção com :

  1. Acrescenta-se uma aresta para cada .
  2. Se , acrescenta-se a aresta .

O grafo dos seguintes tem um caminho se e só se .

Continuando com o mesmo exemplo:

Exemplo de Grafo dos Seguintes
Exemplo de Grafo dos Seguintes

donde resulta

O próximo passo consiste em determinar os primeiros símbolos que cada produção gera.

Diretores. Seja uma GIC e . O conjunto dos diretores de é:

Depois de calculados os primeiros e os seguintes, os diretores são facilmente encontrados:

Os diretores permitem facilmente verificar se uma GIC é LL(1):

Teorema dos Diretores. Seja uma GIC. Se, para qualquer variável quaisquer duas produções de tiverem os respetivos diretores distintos, isto é, se para quaisquer duas produções de , então é LL(1)

Exemplos de Aplicação do Teorema dos Diretores


A GIC definida por não é LL(1):

e Como conclui-se que esta gramática não é LL(1).


Um caso mais interessante é a seguinte variante das expressões algébricas, que ilustra a aplicação de algumas transformações:

Para verificar se esta última gramática é LL(1), passo a passo:

Geradores de Vazio

Primeiros

Grafo dos Primeiros
Grafo dos Primeiros

Seguindo as arestas:

Seguintes

Grafo dos Seguintes
Grafo dos Seguintes

Seguindo as arestas:

Diretores

Analisador Sintático

Com os diretores de cada produção calculados, se a gramática for LL(1), é simples implementar manualmente um Analisador Sintático para essa gramática:

def S():
    if seguinte in "(a":
        E()
        consome("#")
    else:
        erro()

def E():
    if seguinte in "(a":
        T()
        X()
    else:
        erro()

def Z():
    if seguinte in "+":
        consome("+")
        T()
        X()
    else:
        erro()

def X():
    if seguinte in "+":
        Z()
    elif seguinte in "#)":
        return
    else:
        erro()

def T():
    if seguinte in "(":
        consome("(")
        E()
        consome("(")
    elif seguinte in "a":
        consome("a")
    else:
        return erro()

def consome(terminal):
    if terminal == seguinte:
        # AVANÇA
        seguinte = ...
    else:
        erro()

def erro():
    # Para o processamento
    ...

Um exemplo deste programa a correr, para analisar a palavra a+a#, é:

"pilha"seguinteresto
S()a+a#
E(); consome(#)a+a#
T(); X(); consome(#)a+a#
consome(a); X(); consome(#)a+a#
X(); consome(#)+a#
Z(); consome(#)+a#
consome(+); T(); X(); consome(#)+a#
T(); X(); consome(#)a#
consome(a); X(); consome(#)a#
X(); consome(#)#(vazio)
consome(#)#(vazio)
(vazio)(nenhum)(vazio)

O resultado deste analisador sintático é "verdade" ou "falso" conforme a palavra dada é, ou não, gerada pela gramática. Este é o resultado esperado mas insatisfatório pois nada diz sobre a derivação, isto é a estrutura, da palavra.

Por exemplo, dada a palavra a+a# é desejável saber, além de que , que a sua derivação esquerda é na gramática inicial.

Conclusão

Este último exemplo mostra que a Análise Sintática está quase resolvida:

As GIC LL(1) são adequadas para representar as linguagens de programação. Além disso, é possível definir algoritmos eficientes para determinar computacionalmente se uma palavra é, ou não, gerada por essa gramática.

No entanto... ainda há por onde melhorar esta situação:

  1. A transformação de uma GIC noutra que seja LL(1) é um passo Ad hoc, que depende de muitas escolhas específicas.
  2. Nessa transformação perde-se a ligação à gramática inicial. Em concreto, olhando para a computação de a+a# não se percebe como a palavra é gerada pelas produções da gramática inicial.
  3. Este processo implica a implementação "manual" das "produções" (human in the middle) mas seria muito melhor que fosse totalmente automático. Isto é, pretende-se definir um programa "geral" Super que aceita como entrada uma gramática G e devolve um certo programa P que, por sua vez, aceita como entrada uma palavra p e calcula se esta é, ou não, gerada por G:
    1. P = Super(G).
    2. P(p) é equivalente a "p é gerada por G".