Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas

Análise Sintática para uma Gramática Livre de Contexto, Notas de estudo de Construção

Um algoritmo para a construção de um analisador sintático para uma gramática livre de contexto, que é capaz de construir a árvore gramatical ou uma sequência de derivação para uma sentença válida ou indicar um erro para uma sentença inválida. O algoritmo utiliza funções como terminal(), stf() e match() para realizar a análise sintática.

Tipologia: Notas de estudo

2022

Compartilhado em 07/11/2022

Picapal_amarelo
Picapal_amarelo 🇧🇷

4.6

(169)

224 documentos

1 / 24

Toggle sidebar

Esta página não é visível na pré-visualização

Não perca as partes importantes!

bg1
Capítulo 8
Analisadores sintáticos
Um analisador sintático para uma gramática
é um programa que aceita como
entrada uma sentença (uma lista de símbolos
) e constrói para a sentença sua árvore
gramatical (ou equivalentemente uma seqüência de derivação) ou, caso a sentença
não pertença à linguagem descrita por
, uma indicação de erro.
Duas técnicas básicas para a contrução de analisadores sintáticos são a constru-
ção ascendente ou a construção descendente. Na contrução ascendente (bottom-
up), o analisador sintático varre a sentença buscando a aplicar produções que permi-
tam substituir seqüências de símbolos da sentença pelo lado esquerdo das produções,
até alcançar como único símbolo restante o símbolo sentencial.
O Algoritmo 8.1 ilustra a estratégia de reconhecimento de sentença baseado em
construção ascendente da árvore sintática. Esse algoritmo recebe como entrada uma
representação da gramática
e a lista
de símbolos terminais que compõem a
sentença. A saída é uma indicação se a sentença pertence (true) ou não (false) à
gramática
. Para a descrição desse algoritmo, as seguintes funções são definidas:
, que recebe uma gramática
como argumento e retorna o seu símbolo sentencial;
e
MATCH(

) retorna uma nova lista de símbolos gerada a partir da aplicação de
alguma regra de G à sentença
. Para tanto, esse procedimento analisa se
para a sentença
, composta pela seqüência de símbolos

(onde
eventualmente
e
podem ser a string vazia), na gramática alguma regra
aplicável
 
. Se houver, o valor de retorno do procedimento é a
lista

; caso contrário, o procedimento retorna uma lista vazia.
Esse algoritmo apresenta duas condições de término possíveis: a primeira quando
a sentença pode ser reduzida ao símbolo sentencial da gramática (condição de su-
cesso) e a segunda quando a sentença não está reduzida ao símbolo sentencial e não
mais regras aplicáveis à sentença (condição de rejeição).
Na construção descendente (top-down), o objetivo é iniciar a análise com uma
lista que contém inicialmente apenas o símbolo sentencial; a partir da análise dos
101
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18

Pré-visualização parcial do texto

Baixe Análise Sintática para uma Gramática Livre de Contexto e outras Notas de estudo em PDF para Construção, somente na Docsity!

Capítulo 8

Analisadores sintáticos

Um analisador sintático para uma gramática

é um programa que aceita como entrada uma sentença (uma lista de símbolos  ) e constrói para a sentença sua árvore gramatical (ou equivalentemente uma seqüência de derivação) ou, caso a sentença não pertença à linguagem descrita por

, uma indicação de erro. Duas técnicas básicas para a contrução de analisadores sintáticos são a constru- ção ascendente ou a construção descendente. Na contrução ascendente ( bottom- up ), o analisador sintático varre a sentença buscando a aplicar produções que permi- tam substituir seqüências de símbolos da sentença pelo lado esquerdo das produções, até alcançar como único símbolo restante o símbolo sentencial.

O Algoritmo 8.1 ilustra a estratégia de reconhecimento de sentença baseado em construção ascendente da árvore sintática. Esse algoritmo recebe como entrada uma representação da gramática

e a lista  de símbolos terminais que compõem a sentença. A saída é uma indicação se a sentença pertence (true) ou não (false) à gramática

. Para a descrição desse algoritmo, as seguintes funções são definidas:  , que recebe uma gramática

como argumento e retorna o seu símbolo sentencial; e

MATCH(

  ) retorna uma nova lista de símbolos gerada a partir da aplicação de alguma regra de G à sentença .^ Para tanto, esse procedimento analisa se para a sentença  , composta pela seqüência de símbolos       (onde eventualmente  e  podem ser a string vazia), há na gramática alguma regra aplicável       . Se houver, o valor de retorno do procedimento é a lista  ; caso contrário, o procedimento retorna uma lista vazia.

Esse algoritmo apresenta duas condições de término possíveis: a primeira quando a sentença pode ser reduzida ao símbolo sentencial da gramática (condição de su- cesso) e a segunda quando a sentença não está reduzida ao símbolo sentencial e não há mais regras aplicáveis à sentença (condição de rejeição). Na construção descendente ( top-down ), o objetivo é iniciar a análise com uma lista que contém inicialmente apenas o símbolo sentencial; a partir da análise dos

101

Analisadores sintáticos 102

Algoritmo 8.1 Contrução ascendente.

ASCENDINGPARSER

  1 declare    2 while true 3 do  REMOVEFIRST  4 if 



 ISEMPTY  5 then return true 6 else INSERT 

  7  MATCH

  8 if ISEMPTY  9 then return false

símbolos presentes na sentença, busca-se aplicar regras que permitam expandir os símbolos na lista até alcançar a sentença desejada. Na construção descendente, o objetivo é obter uma derivação mais à esquerda para uma sentença. Em termos de árvores gramaticais, a construção descendente busca a construção de uma árvore a partir da raiz usando pré-ordem para definir o próximo símbolo não-terminal que deve ser considerado para análise e expansão. Pela forma como a técnica de construção descendente opera, ela não pode ser aplicada a gramáticas com produções recursivas à esquerda, ou seja, que contenham regras da forma  

 

A limitação é que a análise descendente de tal tipo de produção poderia levar a uma recursão infinita na análise pela tentativa de expandir sempre a mesma regra sem consumir símbolo algum da entrada. É possível transformar uma produção recursiva à esquerda em uma recursiva à direita que descreve as mesmas sentenças através da seguinte técnica. Sejam  e  duas seqüências de símbolos que não sejam iniciadas pelo símbolo não-terminal

 e sejam as produções para

 :  

    

Através da introdução de um novo símbolo não-terminal

  , as mesmas sentenças descritas pelas produções acima podem ser descritas pelas produções recursivas à direita:   

     

     

Nos dois casos, as sentenças são formadas por uma ocorrência de  no início seguida por zero ou mais ocorrências de .

8.1. Analisador sintático preditivo 104

Dois procedimentos auxiliares são definidos para a construção dessa tabela. O primeiro, STF, computa os símbolos terminais associados ao início das expansões de cada um dos símbolos da gramática. Esse procedimento é descrito no Algo- ritmo 8.2.

Algoritmo 8.2 Cômputo dos primeiros símbolos terminais de um símbolo gramati- cal. STF

  1 declare 

  

2 declare  ^



3 if TERMINAL

  4 then INSERT 

  5 else if   é uma produção de

6 then INSERT 

  7 if        é uma produção de

8 then    9 while   REMOVEFIRST STF

     10 do    11 if  12 then INSERT 

   13 else CONCAT 

 STF

   14 return 

O cômputo de STF pode também ser aplicado a uma cadeia de símbolos, sendo que neste caso o valor resultante é o primeiro cômputo de STF aplicado a cada sím- bolo da seqüência tal que o resultado não tenha sido . Caso o cômputo de STF para todos os símbolos da cadeia resulte em , este também será o resultado final. O outro procedimento auxiliar deve computar, para cada símbolo não-terminal da gramática

, o conjunto de símbolos terminais que podem estar imediatamente à direita do símbolo especificado em alguma forma sentencial. Essa informação é mantida em uma lista 

 !

 , onde  é o símbolo de interesse. Para construir essas listas, as seguintes regras devem ser aplicados até que se esgotem as possibili- dades de acrescentar algo às listas:

Regra 1: O símbolo sentencial da gramática pode ter como próximo símbolo o de- limitador de fim de sentença; insira o símbolo $ na lista 

 !



.

Regra 2: Se existir uma produção em

da forma

  #"  , então todos os sím- bolos terminais que podem iniciar a expansão de  podem aparecer após " ; insira em 

 $

%"  o conteúdo de STF( ) sem incluir , se estiver presente.

Regra 3: Se existir uma produção em

da forma

  #" , então " termina a expansão de



. O mesmo pode ocorrer para uma produção da forma

 

105 Analisadores sintáticos

 #"  onde a expansão de  pode levar à string vazia . Em qualquer

um desses casos, tudo que está em 

 deve ser incluído em

Com esses procedimentos, a construção da tabela sintática para uma gramática

procede como se segue. Para cada produção

  em

1. Compute STF

. Para cada símbolo  dessa lista, acrescente a produção

  como o valor da tabela para o par de chaves

2. Caso STF

contenha  , acrescente a produção

  à tabela para o par

de chaves

 para cada  em 

Como exemplo, considere a construção do analisador sintático preditivo para a gramática da Figura 7.1. Como essa gramática é recursiva à esquerda, o primeiro passo nessa construção é construir a gramática equivalente sem esse tipo de recursão. O resultado da aplicação da técnica para eliminar a recursão à esquerda resulta na

seguinte gramática: 

Para esta gramática, o cômputo de STF() para cada um dos símbolos não-terminais resulta em:

STF

  STF   STF   

STF

STF 

Como STF() para um símbolo terminal é o próprio símbolo, esses valores não são aqui apresentados.

A aplicação das regras para a construção das listas 

 resulta, para cada

símbolo não-terminal:

A construção da tabela sintática para essa gramática analisa cada uma das suas produções:

107 Analisadores sintáticos

Tabela 8.1 Tabela sintática para o analisador preditivo.

P1 P
P2 P3 P

 P4 P

P6 P5 P6 P

 P7 P

a direita (Left-to-right) e que é utilizada a derivação canônica mais à esquerda (Left- most derivation). O número entre parênteses indica quantos símbolos da sentença precisam ser analisados (lookahead) para a tomada de decisão no processo de reco- nhecimento. Uma gramática com duas produções

  e

  é LL(1) se apresentar as

seguintes propriedades:

1.  e  não podem derivar ao mesmo tempo seqüências que tenham início pelo

mesmo símbolo terminal;

2. Apenas um dos dois,  ou  , podem derivar ; e

3. Se uma das produções deriva , a outra não pode derivar qualquer seqüência

de símbolos que tenha início com um símbolo presente em 

8.1.2 Algoritmo de reconhecimento de sentença

O Algoritmo 8.3 descreve o analisador sintático preditivo não-recursivo que re- conhece sentenças para a gramática especificada. Durante o processamento, o pro- grama utiliza uma estrutura de pilha para acomodar os símbolos sob análise. O programa utiliza dois procedimentos auxiliares. O primeiro, SELECT, recebe como argumentos a descrição da gramática, um símbolo não-terminal (que está sob análise para expansão) e o próximo símbolo da sentença, retornando uma lista com os símbolos do lado direito da produção que deve ser aplicada para expandir o símbolo analisado, ou o valor nulo no caso de erro. Internamente, esse procedimento faz uso da informação contida na tabela sintática. O outro procedimento auxiliar é TERMINAL, que recebe como argumentos a des- crição da gramática e um de seus símbolos, retornando verdadeiro se este for um símbolo terminal ou falso, se for não-terminal. A seqüência de produções usadas para reconhecer a sentença é registrada em uma lista

, que é retornada ao final do algoritmo. Um valor de retorno nulo indica que a sentença não foi reconhecida para a gramática indicada.

Considere o reconhecimento da sentença %     usando esse algoritmo.

Na condição inicial, a pilha contém o delimitador $ e o símbolo sentencial

; a lista com a sentença contém os sete símbolos terminais e o delimitador $:

8.1. Analisador sintático preditivo 108

Algoritmo 8.3 Analisador sintático baseado na técnica de construção descendente.

PREDPARSER

1 declare



2 declare ^



3 declare

4 PUSH 

5 PUSH 

6 repeat 7

 POP 

 REMOVEFIRST 

9 if TERMINAL





10 then if



11 then return NIL 12 else

 SELECT

13 if

 NIL

14 then return NIL

15 else INSERT 

16 APPEND



17 repeat

18 PUSH 

REMOVELAST

19 until ISEMPTY

20 until

21 return

8.2. Analisador de deslocamento e redução 110

Tabela 8.2 Final da seqüência de reconhecimento da sentença.

Produção Pilha Lista

8.2 Analisador de deslocamento e redução

A estratégia de análise sintática por deslocamento e redução é baseada na técnica de reconhecimento de sentenças por construção ascendente. Nessa estratégia, sím- bolos terminais da sentenças são lidos um a um; a cada símbolo lido, o analisador decide se prossegue com a leitura (desloca) ou se é possível aplicar uma produção aos símbolos previamente lidos para substituí-los por um símbolo não-terminal da gramática (reduz). O procedimento conclui com sucesso se toda a sentença foi lida e apenas o símbolo sentencial resulta da aplicação de todas as reduções. Da mesma forma que para o analisador preditivo, a decisão a ser tomada pelo ana- lisador é apoiada em uma estrutura de dados gerada a partir da análise da gramática. A construção dessa estrutura é apresentada a seguir e seu uso no reconhecimento de sentenças, na seqüência.

8.2.1 Construção da tabela SR

A base para a operação de reconhecimento neste tipo de analisador é a Tabela de Deslocamento e Redução ou Tabela SR (shift-reduce) , as duas ações básicas desem- penhadas pelo analisador durante a análise de uma sentença. Essa tabela determina, a partir do último símbolo resultante das ações efetuadas sobre os símbolos já lidos (que pode ser tanto um símbolo terminal como um não-terminal) e do próximo sím- bolo terminal presente na sentença, se o próximo passo da análise é ler o próximo símbolo, reduzir os símbolos já lidos ou se não há ação a ser tomada. Para construir essa tabela, as produções da gramática são analisadas para obter

111 Analisadores sintáticos

as relações de precedência simples entre os símbolos gramaticais, acrescidos do

delimitador de sentenças $. Para dois símbolos e  , as relações de precedência

definidas são  ( confere precedência a  ) e   ( tem precedência

sobre  ).

Para obter as relações “confere precedência” entre os símbolos de uma gramática

, as seguintes regras são aplicadas:

Regra 1: 

, ou seja, o símbolo delimitador de sentença confere precedência

ao símbolo sentencial da gramática.

Regra 2:  se existe alguma produção de

na forma      , ou seja,

onde aparece à esquerda de  no lado direito da produção.

Regra 3:  se  , onde  é um símbolo não-terminal, e existe alguma

produção para  em

onde  é o primeiro símbolo do lado direito,  

As relações “tem precedência sobre” entre os símbolos de

são obtidas pela aplicação das seguintes regras:

Regra 4:

 , ou seja, o símbolo sentencial da gramática tem precedência

sobre o delimitador de sentença.

Regra 5:   se, para algum símbolo não-terminal  ,   e existe uma

produção para  em

cujo último símbolo é ,    .

Regra 6:   se, para algum símbolo não-terminal  ,   e existe uma

produção para  em

cujo último símbolo é ,    .

Regra 7:   se, para algum símbolo não-terminal  ,   e existe uma

produção para  em

cujo primeiro símbolo é  ,    .

Deve-se observar que  não implica que   . Também pode ser

verdade para alguma gramática que  ao mesmo tempo que  ou .

Assim, não se deve confundir as propriedades dessas relações, apesar da semelhança de notação, com aquelas das bem conhecidas relações de ordem “menor ou igual” e “maior”. Uma vez determinado o conjunto completo das relações de precedência simples, é possível construir diretamente a tabela de deslocamento e redução. Nessa tabela, a

chave é composta por dois símbolos



 da gramática estendida com o delimitador

de sentença. O primeiro estará associado ao estado corrente da análise da sentença, podendo portanto ser um símbolo qualquer. O segundo símbolo da chave é um sím-

bolo terminal, que estará associado ao próximo símbolo da sentença. Se

 ,

então o valor na tabela para a chave



conterá a indicação de que a próxima ação

deve ser a leitura do próximo símbolo da sentença. Se 

 , então o valor da chave indicará que a próxima ação do analisador deve ser a redução dos últimos símbolos lidos pela aplicação de uma produção da gramática.

113 Analisadores sintáticos

A última regra, Regra 7, deriva novas relações de “tem precedência sobre” a partir dessas relações onde há um símbolo não-terminal no lado direito. Particularmente para esse exemplo não há nenhuma relação dessa forma e portanto nenhuma nova relação pode ser derivada. Concluída a análise das relações de precedência para a gramática, é possível construir a sua tabela de deslocamento e redução (Tabela 8.3) usando as relações

 ou  onde  é um símbolo terminal. Nessa tabela, a entrada “S” (shift)

indica que a ação deve ser de leitura do próximo símbolo da sentença, enquanto que a entrada “R” determina a redução dos símbolos já lidos. Para as entradas em branco não há uma ação que possa ser tomada que leve ao reconhecimento da sentença.

Tabela 8.3 Tabela de deslocamento e redução.

$ S S
S S S R

 R S R R

 R R R R

  R R R R

 S S

S S

S S

 R R R R

Observe na Tabela 8.3 que a entrada para

 recebeu uma marcação especial,

pois essa situação — os símbolos já analisados resultaram no símbolo sentencial e a sentença chegou ao fim — determina a condição de reconhecimento da sentença. Para algumas gramáticas, a construção da tabela de deslocamento e redução pode levar a situações onde mais de uma ação poderia ser tomada para um dado estado e próximo símbolo da sentença. Essa tabela não terá entrada duplicadas se a gramática for uma gramática de operadores , para a qual nenhuma produção tem do lado di-

reito dois símbolos não-terminais adjacentes, e se nenhuma produção tiver  do lado

direito.

8.2.2 Algoritmo do analisador de deslocamento e redução

O analisador de deslocamento e redução trabalha com duas estruturas de dados auxiliares, além da tabela de deslocamento e redução. A primeira delas é a lista de símbolos terminais a analisar, que contém inicialmente a sentença submetida à análise delimitada ao final pelo símbolo $. A outra estrutura é uma pilha com os símbolos já analisados, os quais podem ter sido eventualmente substituídos por sím- bolos não-terminais pela aplicação de produções da gramática. Portanto, a pilha pode conter qualquer símbolo, terminal ou não-terminal, do alfabeto da gramática.

8.2. Analisador de deslocamento e redução 114

Há dois principais procedimentos auxiliares utilizados nesse algoritmo. O pri- meiro é NEXTACTION(), que determina qual a próxima ação em função do estado corrente do analisador e da consulta à tabela de deslocamento e redução. Seus ar- gumentos são, além da referência à gramática, um símbolo que corresponde ao topo da pilha e o próximo símbolo da sentença. Seu valor de retorno foi definido ser do

tipo Action , que pode assumir os valores  ,

ou o valor nulo para indicar uma situação de erro.

O outro procedimento auxiliar usado na descrição do algoritmo é REDUCE(), que recebe como argumentos a referência à gramática e à pilha. Esse procedimento retira do topo da pilha os símbolos que podem ser utilizados para combinar, na ordem correta, com o lado direito da produção aplicável no estado atual. Seu valor de retorno é o símbolo resultante, aquele do lado esquerdo da produção aplicada.

O Algoritmo 8.4, que descreve o procedimento do analisador, recebe como argu- mentos a descrição da gramática

e a lista  com a sentença a ser analisada. O valor

de retorno é verdadeiro, se a sentença pertence à gramática, ou falso, caso contrário.

Algoritmo 8.4 Analisador sintático por deslocamento e redução.

PARSESR

1 declare



2 declare   

 

3 declare  



 REMOVEFIRST 

6   NEXTACTION

7 while 



8 do PUSH 

9 if   

10 then

11

 REMOVEFIRST 

12 else if  

13 then

 REDUCE

14 else return false

15   NEXTACTION

16 return true

A aplicação desse algoritmo é ilustrada através do reconhecimento da sentença

%    . No estado inicial, a pilha está vazia e  contém todos os símbolos da

sentença:

Antes do início da primeira iteração,

recebe o delimitador $ e

recebe o pri-

8.2. Analisador de deslocamento e redução 116

Tabela 8.4 Reconhecimento da sentença %     por deslocamento e redução.

        S

       S

        R

R

  R

S

      S

      R

R

  R

S

    R

  R

  S

     S

     R

R

  R

R

117 Analisadores sintáticos

é a situação de conflito reduzir ou deslocar e a outra, quando pelo menos duas re- gras são aplicáveis em uma situação de redução, é a situação de conflito reduzir ou reduzir. O método LR de análise é o mais geral que pode ser aplicado a todas as lingua- gens e gramáticas passíveis de análise determinística. Seu nome deriva-se do fato de que a análise é realizada a partir de uma leitura dos símbolos da esquerda para a direita ( Left to right ) e que a derivação canônica mais à direita é obtida ( Rightmost derivation ). Uma gramática LR(  ), usada como base de um analisador ascendente, é uma na qual as situações de conflito podem ser resolvidas pela verificação dos símbolos já lidos até o momento e pela visão de uma quantidade limitada a no máximo  símbolos adiante (o chamado lookahead ). Na prática, o valor de  é geralmente limitado a 0 ou 1 sem perda de generalidade na aplicação do método. Embora haja gramáticas LR(2) que não são gramáticas LR(1), há um resultado teórico que diz que toda linguagem gerada por uma gramática LR( ) pode ser também gerada por uma gramática LR(1).

8.3 Geradores de analisadores sintáticos

Como ocorre na construção de analisadores léxicos, a construção de programas analisadores sintáticos é usualmente suportada por ferramentas para a geração auto- mática de programas a partir de uma especificação. Uma tradicional ferramenta de criação de analisadores sintáticos é yacc (Yet Another Compiler-Compiler) , oriunda do ambiente de desenvolvimento de software do sistema operacional Unix. Assim como a ferramenta lex (Seção 6.4), yacc re- cebe como entrada um arquivo de especificação de uma gramática e gera como saída um módulo com código-fonte em C contendo uma rotina que realiza o reconheci- mento de sentenças segundo essa gramática.

8.3.1 Especificação da gramática

O arquivo de entrada para yacc, que por convenção recebe a extensão .y, é estruturado em três seções. Como na definição de arquivos lex, essas três seções — definições, regras da gramática e código do usuário — são separadas pelos símbolos %%. A especificação das regras da gramática utiliza uma notação próxima de BNF (Seção 4.3.2). Cada produção é expressa na forma

simb : exp ;

onde simb é um símbolo não terminal e exp é a sua expansão em termos de outros símbolos da gramática. A expansão pode conter símbolos terminais e não-terminais, que por convenção são representados usando letras maiúsculas e minúsculas, respec- tivamente.

119 Analisadores sintáticos

determina que uma expressão A OP B OP C será interpretada como (A OP B) OP C, enquanto que se a declaração tivesse sido

%right OP

a interpretação seria A OP (B OP C). A declaração

%nonassoc OP

determinaria que a expressão A OP B OP C estaria incorreta, pois o operador não é associativo. A precedência dos operadores também é definida através dessas declarações. Operadores definidos através da mesma linha de declaração, como

%left OP1 OP

têm a mesma precedência. Para aqueles definidos em linhas distintas, as últimas declarações têm maior precedência. O símbolo terminal error é pré-definido, podendo ser utilizado como a última expansão de um símbolo caso a aplicação deseje determinar um curso de ação especí- fico em uma situação de não-reconhecimento de uma sentença a partir das expansões previamente definidas para o símbolo.

8.3.2 Manipulação das sentenças reconhecidas

Reconhecer que uma seqüência de símbolos é uma sentença válida em uma gra- mática é parte essencial do processo de compilação, porém pouco uso teria se sim- plesmente uma indicação de validade fosse retornada sem nenhuma possibilidade de manipulação adicional das expressões. No caso de yacc, essa possibilidade está associada à definição de ações semânticas. Uma ação semântica em yacc é definida através de um bloco de expressões em C associado à definição de produções para um símbolo não-terminal:

symb : expansão { ação } ;

A definição do corpo da ação pode conter referências aos valores semânticos de cada um dos símbolos da produção. O valor semântico de um token está associado a um valor associado ao símbolo, que pode ser por exemplo um valor numérico de uma constante ou a string associada a um identificador. O valor semântico do token pode ser referenciado na expressão C através de pseudo-variáveis com nome $ , onde  determina a posição do token na expansão. A variável $$ referencia o valor semântico resultante para o símbolo sendo definido. Por exemplo,

expr : expr ’+’ expr { $$ = $1 + $3 } ;

8.3. Geradores de analisadores sintáticos 120

atribui à expressão reduzida o valor semântico que é a soma dos valores semânti- cos do primeiro e do terceiro componentes da expansão, que estão separados pelo segundo componente, ’+’. Se nenhuma ação for definida para uma produção, a ação semântica padrão — { $$ = $1; } é assumida. O tipo associado a valores semânticos é definido pela macro YYSTYPE, que é inicialmente definida como int. Para modificar esse padrão, pode-se modificar essa definição através de uma declaração C na primeira seção do arquivo que define a gramática, como por exemplo

%{ #define YYSTYPE double %}

Em aplicações que necessitem manipular tokens com diferentes tipos de valores semânticos, a declaração union deve ser utilizada para definir quais são os tipos de valores possíveis. Por exemplo, em uma aplicação que manipula valores inteiros e reais a seguinte declaração estaria presente:

%union { int ival; double fval; }

Essa declaração determina que a coleção de tipos de valores permitidos é composta por valores com nome ival ou fval, respectivamente para valores inteiros e reais — no código C, uma estrutura com conteúdos alternativos ( union ) será criada. Esses mesmos nomes são utilizados para qualificar a definição de tokens da gramática, como em

%token INTEGER %token REAL

Quando uma coleção de tipos de valores é utilizada, é preciso determinar também qual o tipo para o símbolo não-terminal para o qual a expressão está sendo reduzida. Para esse fim, yacc define a declaração type:

%type expr

8.3.3 Desenvolvimento de uma aplicação

Nesta seção descreve-se o procedimento para desenvolver uma aplicação, usando a ferramenta bison, que realiza a análise sintática de um arquivo de entrada. A fer- ramenta bison é uma implementação de yacc disponível para diversas plataformas e distribuída, assim como flex, sob a licença de software GNU da Free Software Foundation.