Você está na página 1de 40

UNIVERSIDADE FEDERAL DO PARÁ

INSTITUTO DE TECNOLOGIA

FACUDADE DE ENGENHARIA DA COMPUTAÇÃO

O ANALISASOR SINTÁTICO BISON

Jonilton Marcos Barros Serejo

Luciana Cristina da Silva Rêgo

Philipe Terroso de Lima

Thiago Fernandes da Silva Oliveira

Belém

2009
RESUMO

A construção de compiladores talvez não seja um assunto tão presente em

nossos dias, no entanto, o seu estudo ainda se reveste de grande importância, agora mais

relacionado aos aspectos considerados de base para o aprendizado de linguagens de

programação. O presente trabalho, além de uma visão geral sobre compiladores, discute

mais especificamente a fase onde realiza-se o processamento do código-fonte e a

preparação e validação deste antes da tradução para uma linguagem de mais baixo nível.

Será apresentada a ferramenta Bison, cujas funcionalidades podem ser aplicadas numa

vasta área da computação, pois não tem sua utilidade voltada somente para os

propósitos de compiladores, mas também fornecem a funcionalidade de análise e

interpretação de um arquivo de entrada para qualquer programa que receba como

entrada dados que possam ser descritos em uma linguagem formal, tais como editores

de textos, conversores de arquivos e interpretadores em geral.


SUMÁRIO

INTRODUÇÃO

I. FASES DE UM COMPILADOR

I. Análise Léxica

II. Análise Sintática

III. Análise Semântica

IV. Geração do Código Intermediário

V. Otimização de Código Intermediário

VI. Geração de Código Final

II. ANALISADOR SINTÁTICO BISON

I. Descrição Teórica

I. Histórico

II. Os Conceitos do Bison

 Linguagens e Gramáticas Livres de Contexto

 Regras Formais para Entradas do Bison

 Valores Semânticos

 Ações Semânticas

 Saída do Bison: o Arquivo Analisador

 Passo-a-passo no Uso do Bison

 O Formato de uma Gramática do Bison

II. Descrição Prática

I. Exemplo simples de utilização: calculadora.y


II. Exemplo de utilização em conjunto com a ferramenta Flex:

cadastro.l e analisador.y

CONCLUSÃO

APÊNDICE A - Instalação das ferramentas necessárias para a geração de

um analisador sintático, parser, em Sistema Operacional Windows XP

APÊNDICE B - Formato padrão do número de matrícula para alunos de

Engenharia de Computação na Universidade Federal do Pará

APÊNDICE C – Mais exemplos de códigos

REFERÊNCIAS BIBLIOGRÁFICAS
INTRODUÇÃO

São chamados de compiladores os programas que apresentam a capacidade

receber como entrada um arquivo texto que represente uma linguagem de programação

de alto nível, chamado de código fonte, e possa traduzi-lo em um programa equivalente

em outra linguagem de baixo nível, chamada de código objeto ou linguagem de

máquina. O compilador possui várias fases:

i. A análise léxica identifica seqüências de caracteres que constituem unidades

léxicas denominadas tokens.

ii. A análise sintática verifica se a estrutura gramatical está correta.

iii. A análise semântica verifica se as estruturas sintáticas analisadas fazem sentido

ou não.

iv. O gerador de código intermediário transforma a arvore de derivação em um

pseudo código.

v. Otimização do código aperfeiçoa o código intermediário em termos de

velocidade e espaço em memória.

vi. Gerador de código final gera o código objeto.

O Bison[1] é um analisador sintático de propósito geral que converte uma

notação de uma gramática livre de contexto em um analisador LR (left-to-right) [2] ou

GLR (Generalized Left-to-right Rightmost derivation) [3] para essa gramática. Com o

auxílio deste, pode-se desenvolver analisadores para uma gama de linguagens e

aplicações. Por ser compatível com Yacc (Yet Another Compiler-Compiler) [4], todas

gramáticas escritas no Yacc podem trabalhar sem alterações no Bison. É necessário ter
conhecimento das linguagens de programação C ou C++ para sua utilização, entretanto,

qualquer um acostumado com Yacc pode ser capaz de usá-lo sem problemas.

I. FASES DE UM COMPILADOR

A Figura I mostra todas as fases de um processo de compilação. Este processo é

dividido em duas etapas: front-end e back-end, as quais se subdividem em três fases

cada. Essas fases serão explanadas brevemente a seguir.

Figura I – Fases do processo de compilação

I. Análise Léxica

O analisador léxico (AL) é quem executa a primeira fase de um processo de

compilação. Recebendo como input o código fonte ou programa fonte. Este código

escrito pelo programador encontra-se na linguagem fonte.

O AL recebe como input um fluxo de caracteres, lendo um a um ele identifica e

classifica os tokens. A seqüência de caracteres que formam um token é chamada de


lexema e a sua descrição em palavras chama-se padrão. Ocorre um erro léxico quando

certo token não pode ser definido com os padrões existentes. Existem em certas

linguagens operadores relacionais, os quais necessitam que mais de um caractere seja

lido a fim de ocorrer a devida identificação, é por essa razão que o AL possui também

um buffer. A saída gerada pelo AL é um fluxo de tokens em forma de tuplas.

Exemplo: a + b = 3

Cinco tokens podem ser facilmente identificados se considerarmos a linguagem de

programação C e a saída do AL, em tuplas, para esses tokens seria:

<id,a> <+,> <id,b> <=,> <num,3>

Expressões Regulares e Autômatos Finitos na Análise Léxica

As expressões regulares são a base da análise léxica. São elas que definem os

padrões da linguagem, restringindo-as assim. Os autômatos finitos são utilizados como

ferramenta para determinar se palavra é compatível com determinada expressão regular.

Tabela de símbolos

O AL inicia uma tabela de símbolos que será utilizada por todas as demais fases

do processo de compilação. Tal tabela pode operar em leitura e inserção. Durante o

processo de análise léxica são armazenados na tabela de símbolos todos os tokens

reconhecidos com seus respectivos tipos, atributos e qualquer informação que venha a

ser necessária para as demais fases.

Funcionando como uma sub-rotina do analisador sintático (parser)


O AL envia o conjunto de tokens reconhecido para o parser, enquanto este cuida

da leva recebida, o léxico continua o reconhecimento dos demais tokens do programa

fonte. Assim que o parser termina o processamento do conjunto atual, ele faz um

requerimento de mais tokens ao AL, que pára o reconhecimento que estava fazendo, e

os envia ao parser. Este processo se repete até que o programa fonte termine.

Caso a linguagem possua suporte a macros pré-processados o AL faz as

substituições necessárias em uma leitura prévia. Nesta mesma leitura são removidas as

tabulações, espaços em branco, comentários e qualquer outro(s) caractere(s) não

significativo(s) ao programa.

II. Análise Sintática

O analisador sintático (AS), ou parser, executa a segunda fase de um processo

de compilação a qual é considerada uma das etapas mais importantes do compilador.

Esta fase é a responsável pela verificação do fluxo de tokens repassado pelo AL, tal

verificação determina se este fluxo encontra-se ordenado de forma válida para que esta

sentença faça parte da linguagem definida por uma determinada gramática. Este

processo é feito com o uso de gramáticas livres do contexto (GLC), onde os tokens são

os símbolos terminais da GLC e devem satisfazer às suas regras, as quais descrevem a

linguagem fonte.

Uma vez que a entrada do parser não obedeça a tais regras, o papel do parser é

rejeitar aquele programa, ou seja, indicar a ocorrência de um erro sintático, relatando-os

de forma inteligível e oferecendo suporte para a recuperação de erros a fim de que a

análise seja terminada. A falta de um delimitador, como, por exemplo, um parêntese não

balanceado, é um erro sintático.


A análise de aceitação ou rejeição de certa sentença é feita a partir da construção

de uma árvore de derivação válida de acordo com as regras de derivações da GLC, onde

os símbolos terminais são as palavras-chave, operadores e etc. da linguagem, os

símbolos não terminais são conjuntos de cadeias auxiliares e as produções especificam

como deve ocorrer a troca dos não terminais pelos terminais.

A derivação de uma sentença pode ser feita utilizando derivação mais à esquerda

ou mais à direita, assim como a análise sintática pode utilizar um método descendente

ou ascendente de derivação. A derivação mais à esquerda é aquela em que o símbolo

não terminal a ser derivado é sempre aquele que se encontra mais à esquerda, o que é

análogo à derivação mais à direta.

Os métodos de derivação do analisador sintático são classificados de acordo com

a ordem, ou seja, o método é ascendente é também chamado de buttom-up e o

descendente, top-down. O primeiro inicia nos terminais (folhas) e tenta chegar ao

símbolo não-terminal inicial (raiz) e o segundo segue a ordem inversa. Pode-se então

construir a árvore de derivação de distintas maneiras. É quando estas distintas maneiras

(ao menos uma) produzirem árvores de derivação diferentes que chamamos a gramática

de ambígua.

Na prática, os métodos de análise sintática descendentes não podem processar

gramáticas ditas recursivas à esquerda, por isso, utiliza-se o método de fatoração à

esquerda para eliminar tal recursão.

A análise sintática descendente tenta construir uma árvore gramatical, para a

cadeia de entrada, a partir da raiz. Este processo pode ser feito de diferentes formas, os

três tipos de analisadores sintáticos descendentes (ASD) são: recursivo com retrocesso,

recursivo preditivo e tabular preditivo ou não-recursivo


O ASD recursivo com retrocesso utiliza o método de tentativa e erro na escolha

da regra de produção para o não-terminal a fim de fazer a expansão da árvore de

derivação, partindo da raiz expandindo sempre o não-terminal mais a esquerda.

O ASD recursivo preditivo faz uso de um diagrama de transições para cada não-

terminal de uma dada gramática, onde os rótulos dos lados são tokens e não-terminais.

A utilização deste diagrama facilita a preditividade do analisador sintático já que este

precisa saber qual das regras deverá ser utilizada de acordo com cada cadeia de entrada

e não retroceder. O diagrama de transição de cada não-terminal deve conter um estado

inicial e um estado final além de se comportar como um autômato finito determinístico

(AFD), uma vez que somente uma transição deverá ser seguida. É importante ressaltar

que assim como os demais métodos de análise top-down, gramática não pode ser

recursiva à esquerda.

O ASD preditivo tabular, ou não-recursivo, utiliza uma tabela sintática para

determinar que produção será empregada, além de uma pilha e buffer de entrada.

Ao contrário da análise sintática descendente, a ascendente parte da análise dos

símbolos terminais no sentido do símbolo inicial da gramática, reduzindo sempre o

terminal mais a direita. Por fazer operações de empilhamento (da cadeia para a pilha) e

redução (utilização inversa das regras da gramática) esta análise pode ser chamada

também de análise redutiva.

Um analisador sintático ascendente (ASA) faz uso de uma pilha e uma tabela

sintática para guiar o processo de empilhamento e redução assim como os ASDs. O

ASA é geralmente utilizado para que haja precedência de operadores e opera sobre uma

classe especifica de gramática, as gramáticas de precedencia de operadores, estas

gramáticas não possuem produções que derivem a cadeia vazia ou símbolos não-

terminais adjacentes. Para o reconhecimento de uma cadeia nesta gramática, faz-se


necessario o uso de uma tabela de precedência, além dos elementos já previamente em

uso. É importante frisar que o a comparação de precedência é feita entre o terminal mais

ao topo da pilha e o mais a esquerda da cadeia, ignorando-se os não-terminais.

O analisador sintático produz como saída a árvore de derivação de cada

seqüência de tokens aceita pelas regras da gramática.

III. Análise Semântica

O analisador semântico, visto como uma extensão do analisador léxico e

sintático, recebe como input a árvore de derivação (output da análise sintática) e acesso

total à tabela de símbolos criada pela análise léxica. Utiliza a tradução dirigida pela

sintaxe (TDS) para adicionar o atributo correspondente de cada símbolo gramatical

identificado pelo parser. Ou seja, ocorre um incremento da árvore de derivação de

entrada. A TDS é uma técnica que juntamente com a análise sintática permite a tradução

ou geração de código.

Durante a derivação ou redução, de cada produção que é processada, são

executadas ações semânticas associadas às regras de produção da gramática. Tais ações

podem armazenar símbolos na tabela, gerar ou interpretar código, emitir mensagens de

erro, etc. Associando variáveis aos símbolos da gramática o processo torna-se mais

eficaz, assim tais símbolos passam a conter atributos, ou parâmetros.

Esquemas de Tradução

É uma extensão da GLC utilizada na análise sintática, que funciona de maneira a

atribuir os atributos a cada símbolo gramatical, tais atributos podem ser: valor

numérico, uma string, um tipo de dado ou endereço de memória.

Os atributos podem ser:


 Sintetizados: O valor é computado a partir dos atributos dos filhos daquele nó.

 Herdados: O valor é computado a partir dos valores dos atributos dos irmãos e

pai daquele nó.

Na prática podem-se utilizar definições dirigidas pela sintaxe (DDS) e esquemas de

tradução com algumas repetições. As definições S-atribuídas são muito utilizadas em

compiladores e seus atributos são calculados pro síntese. Definições L-atribuídas

possuem seus atributos compilados por herança e é menos utilizado.

Na DDS cada símbolo gramatical possui um conjunto associado de atributos ou

registro, onde cada campo desse registro é um atributo. O valor de um atributo é

definido por uma regra semântica associada à produção.

IV. Geração do Código Intermediário

A partir da árvore de derivação e seus respectivos atributos o código objeto final

pode ser gerado, porém é necessário um alto nível de abstração e complexidade. Para

amenizar essa tarefa é implementada a geração de código intermediário.

A utilização desta fase trás vantagens, pois possibilita a otimização do código,

simplifica a implementação do compilador, possibilita a tradução do código

intermediário por diversas máquinas. Ou seja, front-end do compilador independente de

máquina, assim como é realizado em JAVA, por exemplo.

V. Otimizador de Código Intermediário

Sua principal função é melhorar o código intermediário, ou seja, aplicar

heurísticas para detectar seqüências e substituí-las por outra equivalente, mas

desprovidas de ineficiência. As técnicas de otimização devem manter o propósito e o


significado original do programa. O otimizador de código elimina redundâncias,

propagações de cópias, desvios desnecessários e códigos não utilizados

VI. Gerador de Código Final

As técnicas empregadas na geração de código podem ser usadas até mesmo sem

uma fase de otimização. O gerador de código alvo é quem realmente se preocupa com a

alocação de memória e escolha de registradores. É ele quem produz o código objeto

final na linguagem alvo, normalmente tal linguagem é a de máquina.

É exigido que o código de saída seja correto e de alta qualidade. Porém gerar um

código ótimo é uma questão que não pode ser resolvida. E para driblar esse problema,

utilizam-se técnicas heurísticas que geram códigos bons, mas não ótimos.

Se um código chegou a esta fase, significa dizer que está limpo de erros. As

características da máquina alvo devem ser conhecidas, como o computador e seu

Assembler ou a máquina hipotética e seu pseudocódigo. Sua saída é um arquivo

executável para a sua máquina alvo.


II. ANALISADOR SINTÁTICO BISON

I. Descrição Teórica

I. Histórico

O Bison é um gerador de analisadores sintáticos que evoluiu por meio da

colaboração de programadores adeptos ao GNU contribuíram imensamente. Foi escrito

inicialmente por Robert Corbett quando teve sua primeira versão registrada em 1988 e

com a colaboração de Richard Stallman a versão 1.20, no ano de 1992, se fez

compatível com o Yacc. Possuiu no decorrer desses vinte e um anos de criação, um total

de dezoito versões registradas, sendo a versão 2.4.1 a última lançada. O lançamento

ocorreu no dia 19 de novembro de 2008.

II. Os Conceitos do Bison

 Linguagens e Gramáticas Livres de Contexto

Para que o Bison analise uma linguagem, ela deve ser descrita

por uma gramática livre de contexto (GLC) [5]. Isso significa que

deve-se especificar um ou mais agrupamentos sintáticos e dar-lhes as

regras para construção de suas partes. Por exemplo, na linguagem C,

um tipo de agrupamento é chamadao de expressão. Uma regra

para fazer uma expressão poderia ser: “Uma expressão pode ser feita

de um sinal de menos e outra expressão” ou “Uma expressão pode


ser um inteiro”. Acontece destas regras serem geralmente recursivas,

no entanto, deve existir pelo menos uma que leve ao fim da recursão.

As GLCs dividem-se em várias subclasses cuja manipulação

pode ser feita pelo Bison, porém este programa é otimizado para o

que são chamadas gramáticas LALR. Analisadores para gramáticas

LALR são deterministicos, isto significa que a próxima regra a se

aplicar em um ponto da entrada é unicamente determinada pela

entrada precedente, porção finita (chamada de ponto de

sincronização) do restante da entrada.

Uma GLC pode ser ambígua, ou seja, pode ter múltiplos

caminhos para se aplicar as regras da gramática para as mesmas

entradas. Mesmo gramáticas sem ambigüidades podem ser não-

determinísticas, pois nem sempre o número de pontos de

sincronização são suficientes para determinar a próxima regra a se

aplicar. Com as declarações apropriadas, o Bison também é capaz de

analisar uma GLC mais generalizada, usando uma técnica conhecida

como análise GLR a fim de que suporte qualquer GLC para qualquer

número de possibilidades de análises de uma dada sequência finita.

Nas regras formais de uma gramática para uma linguagem,

cada tipo de unidade ou agrupamento sintático é nomeado por um

símbolo. Os símbolos que podem ser substituídos por outros são

chamados de símbolos não-terminais; os que não podem, são

chamados de símbolos terminais ou tipos de token. Uma entrada

correspondente a um único símbolo terminal é chamado de token, e


uma parte correspondente a um único símbolo não-terminal é

chamado de agrupamento.

O analisador Bison lê uma sequência de tokes como sua

entrada, e agrupamento de tokens são usados nas regras

gramaticais. Se a entrada é valida, o resultado final é que toda

sequência de tokens é reduzida a um único agrupamento do qual na

gramática é o símbolo inicial. Se não, o analisador reportará um erro

de sintaxe.

 Regras Formais para Entradas do Bison

O arquivo de entrada para análise do Bison, conhecido como

arquivo de gramática, deve seguir uma série de regras formais onde

símbolos terminais e não-terminais devem ter uma representação

padrão. Um símbolo não-terminal numa gramática formal é

representado em Bison como um identificador, assim como ocorre na

linguagem de programação C. Por convenção, ele deve estar em

caixa baixa, como uma expressão, sentença ou declaração.

A representação do Bison para um símbolo terminal também é

chamada de tipo de token. Tipos de token também podem ser

representados como identificadores em C. Por convenção, esses

identificadores devem estar em caixa alta para se distinguir dos não-

terminais. Um símbolo terminal que representa uma palavra-chave

particular da linguagem deve possuir o mesmo nome em caixa alta.

Caso um token seja somente um único caractere (parêntese, sinal de

mais, etc), deve-se utilizar o mesmo caractere de forma literal para


representar tal token. Pode-se representar um símbolo terminal

também com uma constante de cadeia de caracteres de C

contendendo vários caracteres.

As regras da gramática possuem uma expressão na sintaxe do

Bison. O Código I mostra o exemplo de uma regra em Bison para uma

sentença return de C. O ponto e vírgula em questão é um token na

forma literal representando parte da sintaxe de C para a sentença; o

ponto e vírgula e a virgula sem aspas, são pontuações do Bison

usadas em toda regra.

stmt: RETURN expr ‘;’


;
Código I – Regra em Bison para ums sentença return de C

 Valores Semânticos

Uma gramática formal seleciona tokens somente por suas

classificações, por exemplo, se uma regra menciona que o símbolo

terminal ‘integer constant’, isso significa que qualquer constante

inteira é gramaticalmente valida nessa posição. O valor preciso dessa

constante é irrelevante para como analisar a entrada. Entretanto, o

valor preciso é muito importante para indicar que uma entrada já foi

analisada uma vez. Um compilador é inútil se falha em distinguir

entre as constantes no programa. Portanto, cada token em uma

gramática Bison tem tanto valor semântico como tipo de token.

O tipo de token é um símbolo terminal definido na gramática

que contém todas as informações necessárias para decidir onde o

token pode validamente aparecer e como se agrupar com outros


tokens. As regras gramaticais não sabem nada sobre os tokens

exceto seus tipos, o valor semântico possui todo restante da

informação sobre o significado de um token. Por exemplo, um token

de entrada pode ser classificado como tipo INTEGER e ter valor

semântico 4. Outro token pode ter o mesmo tipo de token INTEGER,

mas valor 3989. Quando uma regra gramatical diz que um INTEGER é

permitido, ambos esses tokens são aceitos porque cada um é

INTEGER. Quando o analisador aceita o token, ele mantém o valor

semântico armazenado. Cada agrupamento pode também ter um

valor semântico assim como um símbolo não-terminal.

 Ações Semânticas

Para ser útil, um programa não deve apenas analisar a entrada,

deve produzir uma saída baseada na entrada. Na gramática Bison,

uma regra gramatical pode ter uma ação feita por sentenças em C.

Cada vez que o analisador reconhece uma entrada correspondente à

uma regra, a ação é executada.

Na maioria das vezes, o propósito de uma ação é calcular o

valor semântico de toda a construção dos valores semânticos das

suas partes. No Código II, uma regra diz que uma expressão pode ser

a soma de duas expressões. Quando o analisador reconhece a soma,

cada uma das subexpressões possui um valor semântico que

descreve como foi construída. A ação para essa regra deve criar uma

espécie similar de valor para expressão maior recentemente


reconhecida, ou seja, neste cado, produzir o valor semântico da soma

dos valores das duas subexpressões.

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


;
Código II - Cada vez que o analisador reconhece uma entrada que
pode ser representada por expr, a ação entre chaves é executada.

Em algumas gramáticas, o algoritmo de análise padrão LALR do

Bison não pode decidir se pode aplicar determinada regra em um

dado ponto. Isto é, não é capaz de decidir (com base nas entradas

lida até o momento) qual possível redução (aplicação de uma regra

gramatical) aplicar, ou entre aplicar uma redução ou ler mais entrada

para aplicar a redução mais tarde na entrada. Esses são conhecidos

como conflitos de redução.

Saída do Bison: o Arquivo Analisador

A saída do Bison é um código em C que analisa a linguagem da

gramática descrita. Esse arquivo é chamado analisador Bison. Deve-

se manter em mente que o utilitário Bison e o analisador Bison são

dois programas distintos. O utilitário Bison é um programa cuja a

saída é o analisador Bison, que passa a fazer parte de seu programa.

O trabalho do analisador Bison é agrupar tokens em agrupamentos de

acordo com as regras gramaticais definidas no arquivo de gramática

que serviu como entrada para o Bison. Esta tarefa é realizada

executando as ações para as regras gramaticais definidas

anteriormente.

Os tokens vêm de uma função chamada análise léxica que deve

ser fornecida de alguma forma, pode ser através de uma função


escrita em C no arquivo de entrada (Seção II.II.I) ou com o auxílio de

analisadores léxicos (Seção II.II.II). O analisador Bison chama a função

de análise léxica toda vez que precisa de um novo token. Ele não

sabe o que há “dentro” dos tokens (embora os seus valores

semânticos possam refletir isso).

O arquivo analisador Bison é um código em C e possui uma

função nomeada yyparse que implementa a gramática. Essa função

não faz um programa completo em C, deve-se completá-la com

funções adicionais (como a de análise léxica). Um programa completo

em C, deve começar pela função principal main, cujo objetivo

principal é a chamada da função yyparse ou o analisador nunca será

executado.

Além dos nomes de tipos de tokens e os símbolos que se

escrevem nas ações, todos os símbolos definidos no arquivo do

analisador Bison começam com ‘yy’ or ‘YY’. Isso inclui funções de

interface com o analisador, como a função de análise léxica yylex, a

função de reportagem de erro yyerror e a função de análise em si

yyparse. Portanto, deve-se evitar o uso de identificadores C que

comecem com ‘yy’ ou ‘YY’ na gramática Bison exceto pelos definidos

até então.

 Passo-a-passo no Uso do Bison

O atual formato de processo no uso de Bison, de uma especificação gramatical

para um compilador ou interpretador funcionando, possui as seguintes partes:


a) Especificar formalmente a gramática de uma forma reconhecida pelo Bison. Para

cada regra gramatical na língua, descrever a ação a ser tomada quando uma

instância dessa regra é reconhecida. A ação é descrita por uma sequência de

instruções C.

b) Escrever um analisador léxico para processar a entrada e passar tokens para o

analisador. O analisador léxico pode ser escrito em C. Poderia ser feito também

usando o Lex[6] ou Flex[7].

c) Escrever uma função que chame o analisador produzido pelo Bison.

d) Escrever uma rotina de reportagem de erro.

Para tornar este código-fonte executável, deve-se seguir estes passos:

a) Executar o Bison sobre a gramática para produzir o analisador.

b) Compilar o código de saída do Bison, bem como quaisquer outros arquivos de

código.

c) Lincar os arquivos objetos para produzir o produto finalizado.

 O Formato de uma Gramática do Bison

O arquivo de entrada para o utilitário Bison é um arquivo de

gramática Bison. Tal arquivo deve conter três seções distintas, a forma geral de

um arquivo de gramática Bison é mostrada em Código III.

% {
Declarações em C
%}
Declarações Bison
%%
Regras de gramática
%%
Código em C adicionais
Código III – Forma geral de um arquivo de gramática Bison

O ‘%%’ ,’%{‘ e ‘%}’ são pontuações que aparecem em todo arquivo de

gramática Bison para separar as seções. Na primeira seção são inseridas definições de
tipos e variáveis a serem usadas nas ações semânticas Pode-se usar também comandos

de pré-processamento para definir macros usadas no arquivo e usar #include para

incluir arquivos de cabeçalho que possam conter outras informações exigidas pelo

parser que está sendo criado.

A segunda seção contêm a definição da gramática regular, a descrição da

precedência dos operadores e os tipos de dados de valores semânticos dos diversos

símbolos da gramática.

As regras gramaticais definem como se dará a construção de cada símbolo

terminal a partir dos tokens que o compõe. Juntamente a essa seção estão as ações

semânticas associadas a cada conjunto de token reconhecido pela gramática.

A terceira seção do arquivo pode conter códigos que o programador ache necessário,

como a função main, yylex e etc. Normalmente as funções declaradas na primeira

parte desse arquivo são codificadas nessa seção.

II. Descrição Prática

É valido resaltar que os exemplo mostrados a seguir foram testados e

desenvolvidos com o auxílio das ferramentas Bison e Flex disponiveis em [8,9] e

instaladas como descrito no Apêndice A.

I. Exemplo simples de utilização: calculadora.y

Neste exemplo, a função que executa a análise léxica é criada manualmente e

encontra-se na terceira seção do arquivo de entrada para o Bison. O arquivo analisador

que será gerado com o código mostrado a seguir (Código IV) além de reconhecer as

operações matématicas definidas em sua gramática (adição, subtração, multiplicação

divisão e exponenciação), irá também executá-las, mostrando na tela o resultado.


O analisador lexico, representado pela função yylex, retorna um numero

double na pilha e um token NUM ou o código ASCII do caractere lido se não for um

número. Todos os espaçoes em branco são ignorados e ao atingir o final do arquivo de

entrada retornará 0.

/* Calculadora com precedencia de operadores */

%{
#define YYSTYPE double
#include <math.h>
%}

%token NUM

%% /* Regras Gramaticais e Ações Semânticas */

input: /* vazio */
| input line
;

line: '\n'
| E '\n' { printf ("\t%.10g\n", $1); }
| error '\n' { yyerrok; }
;

E:
E '+' T { $$ = $1 + $3; }
| E '-' T { $$ = $1 - $3; }
| T { $$ = $1; }
;

T: T '*' F { $$ = $1 * $3; }
| T '/' F { $$ = $1 / $3; }
| T '^' F { $$ = pow($1,$3) }
| F { $$ = $1; }
;

F: NUM { $$ = $1; }
| '(' E ')' { $$ = $2; }
;

%
#include <stdio.h>
#include <ctype.h>

int yylex (void)


{
int c;

/* pula espaços em branco */


while ((c = getchar ()) == ' ' || c == '\t')
;
/* processa numeros */
if (c == '.' || isdigit (c))
{
ungetc (c, stdin);
scanf ("%lf", &yylval);
return NUM;
}
/* retorna end-of-file */
if (c == EOF)
return 0;
/* returna o caractere */
return c;
}

/* Função chamada pelo yyparse quando ocorre algum erro */


int yyerror (const char *s)
{
printf ("%s\n", s);
}

int
main (void)
{
return yyparse ();
}
Código IV – Código do arquivo calculadora.y na íntegra

O arquivo analisador é gerado com uso da linha de comando bison –o

calculadora.c calculadora.y. Para que este pudesse ser executado, utilizou-

se o compilador GCC (GNU Compiler Colection)[10] através da linha de comando gcc

–o calculadora calculadora.c . Mais exemplos de códigos com a criação

manual da função yylex podem ser vistos no Apêndice C. Mostra-se abaixo uma

sequência de execuções da calculadora, resultando em erros ou sentenças aceitas com

seus respectivos resultados.

>> calculadora
5+1
6

8-3
5

4*32
128

18/3
6

2^10
1024

5+2*3
11
5%2
syntax error

II. Exemplo de utilização em conjunto com a ferramenta Flex:

cadastro.l e analisador.y

Como dito anteriormente, o Bison age em conjunto com uma função yylex que

retorna um token a cada chamada da mesma. Esta função pode ser criada manualmente,

como no exemplo anterior, ou pode-se fazer uso de ferramentas auxiliares como o Lex

ou Flex. Neste exemplo, utilizaremos a função fornecida com auxilio da ferramenta de

análise léxica Flex, assim como geralmente acontece na maioria das utilizações do

Bison. O arquivo analisador que será gerado com os códigos mostrados a seguir

reconhecerá uma sequência qualquer de nomes ou uma matricula válida para alunos do

curso de Engenharia da Computação da Universidade Federal do Pará (mais

informações em Apêndice B).

O código contido no arquivo de entrada do Flex (cadastro.l) é mostrado em

Código V e assim como o arquivo de entrada do Bison, é dividido em três seções,

marcadas pela pontuação ‘%%’ ,’%{‘ e ‘%}’. No entanto, o código apresentado

possui apenas duas seções devido a não necessidade de códigos adicionais na linguagem

C.

%{
#include "cabecalho.h"
#include "analisador.h"
%}

/* Definições */
STRING [A-Za-z][a-z ]*
BRANCOS [ \n\r\t]+
DIGITO [1-9]
CODIGO_ZERO [0]
CODIGO_OITO [8]
ANY_CHAR .

%option case-sensitive
%% /* Regras */

{BRANCOS} /* ignora */

"MATRICULA" { return T_MATRICULA; }


"NOME" { return T_NOME; }

{CODIGO_ZERO} { return ZERO; }


{CODIGO_OITO} { return OITO; }
{DIGITO} { return T_DIGITO; }

{STRING} { return T_STRING; }

{ANY_CHAR} {
printf("Caracter invalido: '%c' (ASCII=%d)\n", yytext[0],
yytext[0]);
}

Código V – Código do arquivo cadastro.l na íntegra

%{
#include "cabecalho.h"
#include <stdio.h>
%}

/* Definição dos tokens */


%token T_DIGITO
%token ZERO
%token OITO
%token T_MATRICULA
%token T_NOME
%token T_STRING

/* Indica qual o não-terminal inicial da gramática */


%start exp

/*mostra os erros com mais detalhes*/


%error-verbose

%%

/*definição da gramática e suas respectivas regras*/

exp:
T_MATRICULA matricula
| T_NOME string_list
;

matricula: {printf("\nAvaliando numero de matricula...");}


num num ZERO OITO ZERO num num num num ZERO num
{printf("\nMATRICULA ACEITA!\n"); exit(0);}
;

num:
T_DIGITO {printf("...");}
| ZERO {printf("...");}
| OITO {printf("...");}
;

string_list:
string
| string_list string
;

string:
T_STRING {printf("\n\nNOME ACEITO!\n"); exit(0);}
;

%%

/* Código em linguagem C */
void yyerror(const char* errmsg)
{
printf("Sentenca nao reconhecida :( \n");
printf("\n*** Erro: %s\n", errmsg);
}

int yywrap(void) { return 1; }

int main(int argc, char** argv)


{
printf("Uso:\t");
printf("MATRICULA\t [numero]\n");
printf("\tNOME\t\t [nome(s)]\n");
printf("\n Qualquer sequência de nomes eh reconhecida, \nmas
apenas matriculas correspondentes ao curso de\nEngenharia da
Computacao sao aceitas\n");
printf("\nEntre com a sentenca a ser analisada:\n\n");
yyparse();
return 0;
}
Código VI – Código do arquivo analisador.y na íntegra

Nos dois arquivos utilizados para criação do arquivo analisador Bison inclui-se o

arquivo cabecalho.h (Código VII) que contém os protótipos de funções utilizadas

pelos arquivos.
Figura II - Interação entre as ferramentas Flex e Bison. Ambos geram arquivos na
linguagem C que ao serem ligados pelo compilador GCC geram um executável que
realiza a função da análise sintática.

#ifndef __COMMON_H__
#define __COMMON_H__

extern int yylex();


extern int yyparse();
extern void yyerror(const char* s);

#endif
Código VII – Código do arquivo cabecalho.h na íntegra

A integração das ferramentas Bison e Flex (Figura II) é feita através do

arquivo cadastro.l com a inclusão do arquivo analisador.h que será gerado pelo

Bison através da linha de comando bison –d –oanalisador.c

analisador.y . É argumento –d que garante a geração do arquivo analisador.h que

será referenciado pelo código gerado pela Flex através da linha de comando flex –

ocadastro.c cadastro.l.
Após a execução dos comando citados anteriormente, ainda é preciso tornar o

arquivo analisador executável, para isso basta apenas que o arquivo de saída do Flex

seja compilado assim como se fez com o arquivo analisador gerado no exemplo

anterior. A linha de comando será gcc cadastro.c –o analisador. Mais

exemplos de códigos que façam a interação entre as ferramentas Flex e Bison podem ser

vistos no Apêndice C. Mostra-se abaixo uma sequência de execuções do analisador,

resultando em erros ou sentenças aceitas.

>> analisador
Uso: MATRICULA [numero]
NOME [nome(s)]

Qualquer sequência de nomes eh reconhecida,


mas apenas matriculas correspondentes ao curso de
Engenharia da Computacao sao aceitas

Entre com a sentenca a ser analisada:

MATRICULA 08080004301

Avaliando numero de matricula........................


MATRICULA ACEITA!

>> analisador
Uso: MATRICULA [numero]
NOME [nome(s)]

Qualquer sequência de nomes eh reconhecida,


mas apenas matriculas correspondentes ao curso de
Engenharia da Computacao sao aceitas

Entre com a sentenca a ser analisada:

MATRICULA 080790004302

Avaliando numero de matricula.........Sentenca nao reconhecida :(

*** Erro: syntax error, unexpected T_DIGITO, expecting OITO

>> analisador
Uso: MATRICULA [numero]
NOME [nome(s)]

Qualquer sequência de nomes eh reconhecida,


mas apenas matriculas correspondentes ao curso de
Engenharia da Computacao sao aceitas

Entre com a sentenca a ser analisada:

NOME philipe
NOME ACEITO!

>> analisador.exe
Uso: MATRICULA [numero]
NOME [nome(s)]

NOME 3Luciana
Sentenca nao reconhecida :(

*** Erro: syntax error, unexpected T_DIGITO, expecting T_STRING

CONCLUSÃO

A ferramenta Bison, embora seja bastante antiga, sobreviveu a diversas gerações

de sistemas operacionais e até mesmo as mudanças de paradigmas de programação. Sua

importância foi preservada principalmente por estar focada desde sua primeira versão

num conceito moderno, considerado dos mais importantes na atualidade, que é a

capacidade de permitir uma elevada abstração nas tarefas de desenvolvimento.


Nesse contexto, o Bison continua sendo extremamente útil ao processo de

desenvolvimento de qualquer programa que exija funções de interpretador ou

compilador, tendo como requisito apenas uma descrição formal da linguagem,

oferecendo a confiabilidade e robustez adquirida ao longo de décadas de melhoramentos

constantes.

APÊNDICE A - Instalação das ferramentas necessárias para a geração de um

analisador sintático, parser, em Sistema Operacional Windows XP

Instalando o Bison

Após realizar o download das ferramentas open source Flex e Bison nas páginas

do desenvolvedor [7,8], ir ao diretório aonde o aplicativo foi armazenado e dar um

duplo clique com o botão esquerdo do mouse. Esta ação iniciará a instalação.
Clique em Next >, marque a caixa “I accept the agreement”, em seguida Next >,

então você verá a tela abaixo:

Por padrão Windows, qualquer novo programa é instalado, por default, no

diretório C:\Arquivos de programas\. No entanto, isso não pode ocorrer para a

ferramenta Bison, pois um dos arquivos essencias para a execução correta do programa

Bison não consegue ser achada devido aos espaços em branco contidos no caminho do

diretório. Modifica-se o diretório para C:\GnuWin32 ou qualquer outro que não possua

espaços em branco.
Em seguida avance quatro vezes clicando no botão Next >. Logo após clique em

Install, e Finish. Neste momento, a ferramenta Bison encontra-se instalada.

Observação: A instalação da ferramenta Flex é análoga a do Bison e os passos descritos

acima também são válidos para instalação para sistemas operacionas Windows Vista.

Definindo Variáveis de Ambiente

Clique com o botão direito do mouse sobre o ícone do Meu computador, siga

com o cursor até Propriedades, vá até a aba Avançado e então a Variáveis de ambiente.

Clique com o botão esquerdo do mouse na linha onde se encontra a palavra Path, em

seguida clique em Editar. Vá à segunda linha denominada Valor da variável, deixa

como está e siga o cursor do teclado para o final dela, adicione um ‘;’ e em seguida, sem

dar espaço o diretório onde foi instalado o Flex e o Bison, acrescentado de \bin, pois é

onde os executáveis se encontram. Feito isso, as variáveis de ambiente estão definidas.


APÊNDICE B - Formato padrão do número de matrícula para alunos de

Engenharia de Computação na Universidade Federal do Pará

A matrícula é composta por uma combinação de onze dígitos, mostrados a

seguir:

Ano [0-1] Código do Curso Colocação [0-3] Campus [0-1]

[0-2]

 Ano[0-1]: Dezena e unidade, respectivamente, correspondentes ao ano de

realização da matrícula.

 Código do curso[0-2]: são os três dígitos de cadastro do curso na universidade.

No caso da Engenharia de Computação, tais dígitos correspondem a 080.

 Colocação[0-3]: são os dígitos correspondentes à colocação do aluno no

Processo Seletivo Seriado

 Campus[0]-[1]: dígitos correspondentes ao código do campus de origem do

aluno. No caso do campus de Belém, tais dígitos correspondem a 01


APÊNDICE C – Mais exemplos de códigos

Os arquivos de saída do Bison e do Flex para os exemplos abaixos são gerados

utilizando o mesmo padrão de linha de comando utilizados das Seções II.II.I e II.II.II.,

bem como a geração do executável utilizando o compilador GCC.

Utilizando apenas a ferramenta Bison: Uma calculadora mais sofisticada

%{
#include <math.h> /* For math functions, cos(), sin(), etc. */
#include "symtable.h" /* Contains definition of `symrec' */
%}

%union {
double val; /* For returning numbers. */
symrec *tptr; /* For returning symbol-table pointers */
}

%token <val> NUM /* Simple double precision number */


%token <tptr> VAR FNCT /* Variable and Function */
%type <val> exp

%right '='
%left '-' '+'
%left '*' '/'
%left NEG /* Negation--unary minus */
%right '^' /* Exponentiation */

/* Grammar follows */

%%

input: /* empty */
| input line
;

line:
'\n'
| exp '\n' { printf ("\t%.10g\n", $1); }
| error '\n' { yyerrok; }
;

exp: NUM { $$ = $1; }


| VAR { $$ = $1->value.var; }
| VAR '=' exp { $$ = $3; $1->value.var = $3; }
| FNCT '(' exp ')' { $$ = (*($1->value.fnctptr))($3); }
| exp '+' exp { $$ = $1 + $3; }
| exp '-' exp { $$ = $1 - $3; }
| exp '*' exp { $$ = $1 * $3; }
| exp '/' exp { $$ = $1 / $3; }
| '-' exp %prec NEG { $$ = -$2; }
| exp '^' exp { $$ = pow ($1, $3); }
| '(' exp ')' { $$ = $2; }
;
/* End of grammar */
%%

#include <stdio.h>

main ()
{
init_table ();
yyparse ();
}

yyerror (s) /* Called by yyparse on error */


char *s;
{
printf ("%s\n", s);
}

struct init
{
char *fname;
double (*fnct)();
};

struct init arith_fncts[]


= {
"sin", sin,
"cos", cos,
"atan", atan,
"ln", log,
"exp", exp,
"sqrt", sqrt,
0, 0
};

/* The symbol table: a chain of `struct symrec'. */


symrec *sym_table = (symrec *)0;

init_table () /* puts arithmetic functions in table. */


{
int i;
symrec *ptr;
for (i = 0; arith_fncts[i].fname != 0; i++)
{
ptr = putsym (arith_fncts[i].fname, FNCT);
ptr->value.fnctptr = arith_fncts[i].fnct;
}
}

#include <ctype.h>
yylex ()
{
int c;

/* Ignore whitespace, get first nonwhite character. */


while ((c = getchar ()) == ' ' || c == '\t');

if (c == EOF)
return 0;

/* Char starts a number => parse the number. */


if (c == '.' || isdigit (c))
{
ungetc (c, stdin);
scanf ("%lf", &yylval.val);
return NUM;
}

/* Char starts an identifier => read the name. */


if (isalpha (c))
{
symrec *s;
static char *symbuf = 0;
static int length = 0;
int i;

/* Initially make the buffer long enough


for a 40-character symbol name. */
if (length == 0)
length = 40, symbuf = (char *)malloc (length + 1);

i = 0;
do
{
/* If buffer is full, make it bigger. */
if (i == length)
{
length *= 2;
symbuf = (char *)realloc (symbuf, length + 1);
}
/* Add this character to the buffer. */
symbuf[i++] = c;
/* Get another character. */
c = getchar ();
}
while (c != EOF && isalnum (c));

ungetc (c, stdin);


symbuf[i] = '\0';

s = getsym (symbuf);
if (s == 0)
s = putsym (symbuf, VAR);
yylval.tptr = s;
return s->type;
}

/* Any other character is a token by itself. */


return c;
}
calculator.y

/* Data type for links in the chain of symbols. */


struct symrec
{
char *name; /* name of symbol */
int type; /* type of symbol: either VAR or FNCT */
union {
double var; /* value of a VAR */
double (*fnctptr)(); /* value of a FNCT */
} value;
struct symrec *next; /* link field */
};

typedef struct symrec symrec;

/* The symbol table: a chain of `struct symrec'. */


extern symrec *sym_table;

symrec *putsym ();


symrec *getsym ();
symtable.h

Interação entre as ferramentas Bison e Flex: Simulação de um sistema de

aquecimento

%{
#include <stdio.h>
#include <string.h>
#include "heat.h"
%}
%%
[0-9]+ yylval.number=atoi(yytext); return NUMBER;
heater return TOKHEATER;
heat return TOKHEAT;
on|off yylval.number=!strcmp(yytext,"on"); return
STATE;
target return TOKTARGET;
temperature return TOKTEMPERATURE;
[a-z0-9]+ yylval.string=strdup(yytext);return WORD;
\n /* ignore end of line */;
[ \t]+ /* ignore whitespace */;
%%
heat.l

%{
#include <stdio.h>
#include <string.h>

void yyerror(const char *str)


{
fprintf(stderr,"error: %s\n",str);
}

int yywrap()
{
return 1;
}

main()
{
yyparse();
}

char *heater="default";

%}
%token TOKHEATER TOKHEAT TOKTARGET TOKTEMPERATURE

%union
{
int number;
char *string;
}

%token <number> STATE


%token <number> NUMBER
%token <string> WORD

%%

commands:
| commands command
;

command:
heat_switch | target_set | heater_select
;

heat_switch:
TOKHEAT STATE
{
if($2)
printf("\tHeater '%s' turned on\n", heater);
else
printf("\tHeat '%s' turned off\n", heater);
}
;

target_set:
TOKTARGET TOKTEMPERATURE NUMBER
{
printf("\tHeater '%s' temperature set to %d\n",heater,
$3);
}
;

heater_select:
TOKHEATER WORD
{
printf("\tSelected heater '%s'\n",$2);
heater=$2;
}
;

heat.y
REFERÊNCIAS BIBLIOGRAFICAS

[1] Bison – GNU Parser Generator – Manual. Disponível em:

http://userpages.monmouth.com/~wstreett/lex-yacc/bison.html#SEC1

[2] LALR parser - Wikipedia, the free encyclopedia. Disponível em:

http://en.wikipedia.org/wiki/LALR_parser

[3] GLR parser - Wikipedia, the free encyclopedia. Disponível em:

http://en.wikipedia.org/wiki/GLR_parser

[4] Stephen C. Johnson; Yacc: Yet Another Compiler-Compiler. Disponível em:

http://dinosaur.compilertools.net/yacc/index.html

[5] Context-free grammar - Wikipedia, the free encyclopedia. Disponível em:

http://en.wikipedia.org/wiki/Context-free_grammar

[6] M. E. Lesk and E. Schmidt; Lex - A Lexical Analyzer Generator. Disponível em:

http://dinosaur.compilertools.net/lex/index.html

[7] flex: The Fast Lexical Analyzer. Disponível em: http://flex.sourceforge.net/

[8] Bison for Windows, Bison: Yacc-compatible parser generator, versão 2.4.1.

Disponível em: http://gnuwin32.sourceforge.net/packages/BISON.htm

[9] Flex for Windows, Flex: fast lexical analyzer generator, versão 2.5.4. Disponível

em: http://gnuwin32.sourceforge.net/packages/flex.htm

[10] GCC, the GNU Compiler Collection. Disponível em: http://gcc.gnu.org/