• Nenhum resultado encontrado

VISÃO GERAL DO CAPÍTULO

GRAMÁTICAS LIVRES DE CONTEXTO

3.2.3 Exemplos mais complexos

A gramática SomOvelha é muito simples para exibir a potência e a complexidade das CFGs. Em vez disso, vamos retornar ao exemplo que mostrou as deficiências das REs: a linguagem de expressões com parênteses.

1 Expr( Expr ) 2 | Expr Op nome 3 | nome 4 Op → + 5 | - 6 | × 7 | ÷

Começando com o símbolo inicial, Expr, podemos gerar dois tipos de subtermos: subtermos entre parênteses, com a regra 1, ou subtermos simples, com a regra 2. Para gerar a sentença “(a + b) × c”, podemos usar a seguinte sequência de reescrita (2,6,1,2,4,3), mostrada à esquerda. Lembre-se de que a gramática lida com categorias sintáticas, como nome, em vez de lexemas, como a, b ou c.

Regra Forma sentencial

Expr

2 Expr Op nome

6 Expr × nome

1 ( Expr ) × nome

2 ( Expr Op nome ) × nome

4 ( Expr + nome ) × nome

3 ( nome + nome ) × nome

A árvore acima, chamada árvore de análise ou árvore sintática, representa a derivação como um grafo.

Esta CFG simples para expressões não pode gerar uma sentença com parênteses não balanceados ou indevidamente aninhados. Somente a regra 1 pode gerar um parêntese de abertura (abre-parênteses), e também gera o parêntese de fechamento (fecha-parênteses). Assim, ela não pode gerar strings como “a + ( b × c” ou “a + b ) × c)”, e um parser montado a partir da gramática não aceitará tais strings. (A melhor RE na Seção 3.2.1 combinava com essas duas strings.) Claramente, as CFGs nos oferecem a capacidade de especificar construções que as REs não per- mitem.

A derivação de (a + b) × c reescreveu, a cada etapa, o símbolo não terminal restante mais à direita. Esse comportamento sistemático foi uma escolha, mas outras são possíveis. Uma alternativa óbvia é reescrever o não terminal mais à es- querda a cada etapa. O uso das escolhas mais à esquerda produziria uma sequência de derivação diferente para a mesma sentença. A derivação mais à esquerda de (a + b) × c seria:

Regra Forma sentencial

Expr

2 Expr Op nome

1 ( Expr ) Op nome

2 ( Expr Op nome ) Op nome 3 ( nome Op nome ) Op nome 4 ( nome + nome ) Op nome 6 ( nome + nome ) × nome

Derivação mais à esquerda de ( a + b ) × c

Árvore de análise ou árvore sintática Grafo que representa uma derivação.

Derivação mais à direita

Derivação que reescreve, a cada etapa, o não terminal mais à direita.

Derivação mais à esquerda

Derivação que reescreve, a cada etapa, o não terminal mais à esquerda.

associarão significado à forma detalhada da árvore sintática, várias destas árvores implicam vários significados possíveis para um único programa — uma propriedade ruim para uma linguagem de programação ter. Se o compilador não puder ter certeza do significado de uma sentença, ele não poderá traduzi-la para uma sequência de código definitiva.

O exemplo clássico de uma construção ambígua na gramática para uma linguagem de programação é a construção if-then-else de muitas linguagens tipo Algol. A gramática simples para if-then-else poderia ser

1 Comandoif Expr then Comando else Comando

2 | if Expr then Comando

3 | Atribuição

4 | ...outros comandos. . .

Este fragmento mostra que else é opcional. Infelizmente, o fragmento de código

tem duas derivações mais à direita distintas. A diferença entre elas é simples. A primeira tem Atribuição2 controlada pelo if mais interno, de modo que Atribuição2 é executada

A segunda derivação associa a cláusula else com o primeiro if, de modo que Atribuição2 é executada quando Expr1 é falsa, independente do valor de Expr2:

Claramente, essas duas derivações produzem comportamentos diferentes no código compilado.

Para remover essa ambiguidade, a gramática deve ser modificada para incluir uma regra que determina qual if controla um else. Para resolver a gramática do if -then-else, podemos reescrevê-la como:

1 Comandoif Expr then Comando

2 | if Expr then WithElse else

Comando

3 | Atribuição

4 WithElseif Expr then WithElse else

WithElse

5 | Atribuição

A solução restringe o conjunto de comandos que podem ocorrer na parte then de uma construção if-then-else. Ela aceita o mesmo conjunto de sentenças da gramática original, mas garante que cada else tenha uma correspondência não ambígua com um if específico. Ela codifica na gramática uma regra simples — vincular cada else ao if não fechado mais interno; e só tem uma derivação mais à direita para o exemplo.

Regra Forma sentencial Comando

1 if Expr then Comando

2 if Expr then if Expr then WithElse else Comando 3 if Expr then if Expr then WithElse else Atribuição 5 if Expr then if Expr then Atribuição else Atribuição A gramática reescrita elimina a ambiguidade.

A ambiguidade do if-then-else surge de uma deficiência na gramática original. A solução resolve a ambiguidade impondo uma regra que é fácil para o programador se lembrar. (Para evitar a ambiguidade totalmente, alguns projetistas de linguagem reestruturaram a construção if-then-else introduzindo elseif e endif.) Na Seção 3.5.3, vamos examinar outros tipos de ambiguidade e formas sistemáticas de tratá-las.

3 nome + nome × nome

Derivação de a + b × c

Um modo natural de avaliar a expressão é com um percurso em pós-ordem na árvore. Isto primeiro calcularia a + b, e depois multiplicaria o resultado por c para produzir o resultado (a + b) × c. Essa ordem de avaliação contradiz as regras clássicas de precedência algébrica, que avaliaria a expressão como a + (b × c). Como o objetivo final da análise sintática da expressão é produzir o código que a implementará, a gramática da expressão deveria ter a propriedade de montar uma árvore cuja avaliação pelo percurso “natural” produzisse o resultado correto.

O problema real está na estrutura da gramática. Ela trata todos os operadores aritméticos da mesma forma, sem considerar qualquer precedência. Na árvore sintática para (a + b) × c, o fato de que a subexpressão entre parênteses foi forçada a passar por uma produção extra na gramática aumenta um nível à árvore. Este nível extra, por sua vez, força um percurso em pós-ordem na árvore a avaliar a subexpressão entre parênteses antes de avaliar a multiplicação.

Podemos usar este efeito para codificar níveis de precedência de operador na gramática. Primeiro, precisamos decidir quantos níveis de precedência são exigidos. Na gramática de expressão simples, temos três: a precedência mais alta para ( ), a média para × e ÷, e a mais baixa para + e –. Em seguida, agrupamos os operadores em níveis dis- tintos e usamos um não terminal para isolar a parte correspondente da gramática. A Figura 3.1 mostra a gramática resultante, que inclui um único símbolo inicial, Alvo, e uma produção para o símbolo terminal num que usaremos em outros exemplos. Nesta gramática, Expr representa o nível para + e –; Termo, o nível para × e ÷; e Fator, o nível para ( ). Desta forma, a gramática deriva uma árvore sintática para a + b × c que é consistente com a precedência algébrica padrão, como vemos a seguir.

Regra Forma sentencial

Expr

1 Expr + Termo

4 Expr + Termo × Fator

6 Expr + Termo × nome

9 Expr + Fator × nome

9 Expr + nome × nome

3 Termo + nome × nome

6 Fator + nome × nome

9 nome + nome × nome

Derivação de a + b × c

Um percurso em pós-ordem sobre esta árvore sintática primeiro avaliará b × c, e depois somará o resultado a a, processo este que implementa as regras padrão da precedência aritmética. Observe que o acréscimo de não terminais para impor a precedência acres- centa nós interiores à árvore. De modo semelhante, substituir os operadores individuais por ocorrências de Op, os removerá.

Outras operações exigem precedência alta. Por exemplo, os subscritos de array devem ser aplicados antes das operações aritméticas padrão. Isso garante, por exemplo, que a + b[i] avalie b[i] para um valor antes de somá-lo a a, ao invés de tratar i como um subscrito em algum array cujo local é calculado como a + b. De modo semelhante, as operações que mudam o tipo de um valor, conhecidas como type casts em linguagens como C ou Java, possuem precedência mais alta do que a aritmética, porém mais baixa do que os parênteses ou operações de subscrito.

Se a linguagem permitir atribuição dentro das expressões, o operador de atribuição deve ter precedência baixa, a fim de garantir que o código avalie completamente tanto o lado esquerdo quanto o direito da atribuição antes de realizá-la. Se a atribuição (←) tivesse a mesma precedência da adição, por exemplo, a expressão a←b + c atribuiria o valor de b a a antes de realizar a adição, considerando uma avaliação da esquerda para a direita.