BISON_Analisador_Sintático
-
Upload
thiago-fernandes -
Category
Documents
-
view
1.876 -
download
0
Transcript of BISON_Analisador_Sintático
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.
>> calculadora5+1 6
8-3 5
4*32 128
18/3 6
2^10 1024
5+2*3 11
5%2syntax 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.
>> analisadorUso: MATRICULA [numero] NOME [nome(s)]
Qualquer sequência de nomes eh reconhecida,mas apenas matriculas correspondentes ao curso deEngenharia da Computacao sao aceitas
Entre com a sentenca a ser analisada:
MATRICULA 08080004301
Avaliando numero de matricula........................MATRICULA ACEITA!
>> analisadorUso: MATRICULA [numero] NOME [nome(s)]
Qualquer sequência de nomes eh reconhecida,mas apenas matriculas correspondentes ao curso deEngenharia 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
>> analisadorUso: MATRICULA [numero] NOME [nome(s)]
Qualquer sequência de nomes eh reconhecida,mas apenas matriculas correspondentes ao curso deEngenharia da Computacao sao aceitas
Entre com a sentenca a ser analisada:
NOME philipe
NOME ACEITO!
>> analisador.exeUso: MATRICULA [numero] NOME [nome(s)]
NOME 3LucianaSentenca 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 [0-2] Colocação [0-3] Campus [0-1]
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);
elseprintf("\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/