Lex is a computer program that generates lexical analyzers ("scanners" or "lexers"). Lex is commonly used with the yacc parser generator. Lex, originally written by Mike Lesk and Eric Schmidt and described in 1975, is the standard lexical analyzer generator on many Unix systems, and a tool exhibiting its behavior is specified as part of the POSIX standard.
1. As Ferramentas Lex e YACC
Ailton Félix de Lima Filho - Bruno Normande Lins - Michel Alves dos Santos
∗
13 de Dezembro de 2010
Resumo
A principal tarefa de um compilador ou interpretador para uma determinada linguagem
de programação pode ser dividida em duas partes: (1) leitura do código fonte alvo e descoberta de sua estrutura; (2) processamento da estrutura(agora já conhecida) e geração do
módulo executável. Lex e YACC podem gerar fragmentos de programa capazes de solucionar
a primeira parte dessa tarefa. A tarefa de descoberta da estrutura do código fonte novamente
é decomposta em subtarefas: quebra do fonte em tokens(responsabilidade do lex), busca e
validação da hierarquia estrutural do programa(responsabilidade do YACC). Neste modesto
relatório técnico introdutório iremos abordar as principais características desses ferramentas.
∗ Bacharelandos em Ciência da Computação, Universidade Federal do Estado de Alagoas(UFAL), Centro de
Pesquisa em Matemática Computacional(CPMAT), Brasil - Maceió/AL, E-mails: {afdlf2, normandelins, michel.mas}@gmail.com
1
3. Lista de Figuras
1
2
3
4
5
6
7
8
Exemplificando uma Sequência de Compilação. . . . . . . . . . . . . . . . . . . . .
4
Construindo um Compilador com Lex/Yacc. . . . . . . . . . . . . . . . . . . . . . .
5
Arquivos Necessários Para Obtenção do Módulo Executável Final(lex.yy.c e y.tab.c). 6
Ilustração da forma de operação integrada das rotinas yyparse() e yylex(). . . .
6
Processamento de Entradas do Lex. . . . . . . . . . . . . . . . . . . . . . . . . . .
7
Diagramas de Relacionamento Entre as Ferramentas Lex e Yacc. . . . . . . . . . .
8
Exemplificando a Integração Entre as Ferramentas Flex e Bison. . . . . . . . . . .
9
Partes de um Compilador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3
4. 1
Introdução
Antes de 1975 escrever um compilador era uma tarefa árdua que consumia muito tempo.
Então Lesk Schmidt (1975) e Johnson (1975) publicaram trabalhos nos quais abordavam a
geração automática de fragmentos de programa que realizassem as tarefas mais corriqueiras de
um compilador, dessa forma nasceram o lex e o YACC. Esses utilitários facilitaram enormemente
a escrita de compiladores. Detalhes de implementação do lex e YACC podem ser encontrados em
Aho et al. (1986). Essas ferramentas estão disponíveis em:
• Mortice Kern Systems (MKS), www.mks.com
• GNU flex e GNU bison, www.gnu.org
• Cygwin, www.cygwin.com
A versão MKS desses utilitários é um produto comercial de alta qualidade que custa por volta
de U$500,00. Os utilitários da GNU são clones gratuitos do lex (flex) e do YACC(bison), porém
podem ser utilizados em aplicações comerciais. A versão atual do flex é a 2.5.35 e do bison é
a 2.4.1. Cygwin é uma versão dos softwares da GNU portada para plataformas MS Windows c
de 32 bits. As expressões visualizadas na figura [1] podem ser construções que pertençam a um
Figura 1: Exemplificando uma Sequência de Compilação.
determinado código fonte de uma determinada linguagem ou apenas o conteúdo avulso de um
arquivo texto concebido apenas para testes. Lex processará as regras que atendem a leitura dessas
expressões ou patterns e irá gerar código fonte em C para um analisador léxico ou scanner. O
4
5. analisador léxico irá confrontar a entrada textual recebida com as regras ou leis de combinação
inerentes ou internas ao mesmo e baseado nessas regras irá converter a entrada textual fornecida
em tokens, que são nada mais que representações numéricas para cadeias de caracteres.
Quando o analisador léxico encontra identificadores na entrada textual fornecida, ocorre a
inserção desse identificador em uma tabela de símbolos. A tabela de símbolos também pode conter
outras informações a respeito dos elementos identificados tais como seu tipo de dado (inteiro ou
real) ou localização de cada variável em memória.
Figura 2: Construindo um Compilador com Lex/Yacc.
O utilitário YACC não trabalhará apenas com o reconhecimento de padrões, mas com um
formalismo gerador com maior poder de expressão chamado gramática. Uma gramática é um
mecanismo para gerar as sentenças (ou palavras) de uma linguagem(de Alencar Price Toscani,
2004). YACC irá ler uma gramática previamente definida (que pode ser criada através de um
simples editor de texto) e gerar código em C para um analisador sintático ou parser. O analisador
sintático usa as regras de produção da gramática que o permitem processar tokens advindos do
analisador léxico e criar uma árvore de sintaxe. A árvore de sintaxe impõe a hierarquia estrutural
aos tokens. Por exemplo, precedência de operadores e associatividade são aparentes em uma árvore
de sintaxe. O próximo passo, geração de código, pode utilizar-se de uma estratégia chamada depthfirst, que define uma forma de percorrer (visitar) os nós de uma árvore. Nessa estratégia a visita a
um nodo só acontece depois de todos os seus filhos terem sido visitados(de Alencar Price Toscani,
2004; Niemann, 2010). Alguns compiladores produzem código de máquina enquanto outros, como
pode ser visto na figura [1], apenas produzem uma saída em linguagem assembly.
A figura [2] ilustra a convenção de nomes adotada pelos utilitários lex e YACC. Assumindo que
o nosso principal objetivo seja a escrita de um compilador básico chamado bas.exe, prosseguiremos
da seguinte forma. Primeiramente iremos especificar todos os padrões e expressões regulares que
queremos identificar passando-os para o lex através do arquivo bas.l. Seguimos com a definição
da gramática e das regras de produção que serão passadas ao YACC através do arquivo bas.y. Os
comandos necessários para criar nosso compilador bas.exe são listados imediatamente abaixo:
yacc –d bas.y
lex bas.l
cc lex.yy.c y.tab.c –o bas.exe
# create y.tab.h, y.tab.c
# create lex.yy.c
# compile/link
Se estivéssemos utilizando as versões gratuitas de yacc e lex teríamos a seguinte sequência de
comandos:
bison –d bas.y
flex bas.l
gcc lex.yy.c bas.tab.c –o bas.exe
# create bas.tab.h, bas.tab.c
# create lex.yy.c
# compile/link
YACC lê a descrição da gramática definida em bas.y e gera um analisador sintático (parser),
que inclui a função yyparse() no arquivo y.tab.c. A opção -d faz com que o YACC gere definições
5
6. para tokens e os coloque em um arquivo y.tab.h. Lex lê as regras que estão contidas em bas.l e
gera um analisador léxico incluindo a função yylex() ao arquivo lex.yy.c (além de incluir nesse
mesmo arquivo o arquivo de cabeçalho y.tab.h).
Figura 3: Arquivos Necessários Para Obtenção do Módulo Executável Final(lex.yy.c e y.tab.c).
Finalmente, o scanner e o parser são compilados e “link-editados” juntos para criar o executável bas.exe. Da função main executamos a chamada a yyparse() para ativar o compilador.
A função yyparse() automaticamente executa a chamada a yylex() para obter cada token.
Figura 4: Ilustração da forma de operação integrada das rotinas yyparse() e yylex().
2
Lex - A Lexical Analyzer Generator
Lex é um programa que gera analisadores léxicos. Ele é geralmente usado com o yacc, um
gerador de analisadores sintáticos. Escrito originalmente por Eric Schmidt e Mike Lesk, ele é o
gerador de analisador léxico padrão em diversos sistemas Unix.
O lex lê um fluxo de entrada especificando um analisador que mapeia expressões regulares em
blocos de código, e retorna um código fonte implementando o analisador. O gerador é genérico e
pode se adequar a diferentes linguagens de programação. A geração padrão de código é em C.
Apesar de ser software proprietário, versões do lex baseadas no código original da ATT estão
disponíveis em código aberto, como parte de sistemas como OpenSolaris e Plan 9. Outra versão
popular e livre do lex, já mencionada é o flex.
6
7. Figura 5: Processamento de Entradas do Lex.
2.1
Estrutura do Arquivo
A estrutura de um arquivo lex é intencionalmente similar ao de um arquivo yacc. Os arquivos são divididos em três seções, separadas por linhas que contém somente dois símbolos de
porcentagem, como a seguir:
definições
%%
regras
%%
subrotinas
Na seção de definições são construídas as macros e são importadas as bibliotecas escritas em
C. É também possível escrever código C na mesma seção. Já a seção de regras associa padrões
com instruções C, padrões escritos na forma de expressões regulares. Quando o analisador léxico
identifica algum texto da entrada casando com um padrão, ele executa o código C associado. A
tentativa do casamento é sempre gananciosa, isto é, no caso de dois padrões distintos casando
a mesma entrada, o maior deles será usado. O maior deles é o que consome mais caracteres
da entrada. Caso os padrões ambíguos consumam a mesma quantidade de caracteres, o padrão
definido antes é escolhido. Por fim, a seção de subrotinas contém blocos de código C que serão
apenas copiados ao arquivo final. Assume-se que tal código será invocado a partir das regras da
seção de regras. Em programas maiores, é mais conveniente separar esse código final em outro
arquivo.
2.2
Exemplo de um Arquivo Lex
O seguinte exemplo reconhece inteiros da entrada de dados e os imprime na saída padrão.
// Seção de Definição
%{
#include stdio.h
%}
%%
// Seção de Regras
// [0-9]+ casa uma cadeia de um ou mais dígitos
[0-9]+ {
/* yytext é a cadeia contendo o texto casado. */
printf(Inteiro: %sn, yytext);
}
7
8. .
{ /* Ignora outros caracteres. */ }
%%
// Seção de Código em C
int main(void)
{
// Executa o analisador léxico.
yylex();
return 0;
}
O conteúdo acima especificado será convertido em um arquivo da linguagem C. Através da
compilação do arquivo convertido teremos um analisador léxico pronto e funcional que reconhecerá
apenas sequencias que podem sugerir números inteiros. Para a seguinte entrada:
abc123z.!*2ghj6
O analisador imprimirá:
Inteiro: 123
Inteiro: 2
Inteiro: 6
2.3
Relacionamento com YACC
O lex e o gerador de analisadores sintáticos YACC são geralmente usados em conjunto. O
YACC usa uma gramática formal para analisar sintaticamente uma entrada, algo que o lex não
consegue fazer somente com expressões regulares (o lex é limitado a simples máquinas de estado
finito). Entretanto, o YACC não consegue ler a partir de uma simples entrada de dados, ele requer
uma série de tokens, que são geralmente fornecidos pelo lex. O lex age como um pré-processador
do YACC. Segue abaixo dois diagramas de relacionamento entre lex e YACC:
Figura 6: Diagramas de Relacionamento Entre as Ferramentas Lex e Yacc.
A partir do diagrama 1, percebe-se que o lex gera a subrotina yylex() a partir de regras
léxicas, e que o YACC gera a subrotina yyparse() a partir de regras gramaticais. A partir do
diagrama 2, percebe-se que um programa qualquer invoca o analisador sintático para uma fluxo
de entrada. O analisador sintático não consegue analisar entradas, mas sim tokens. Portanto,
cada vez que ele precisa dum token, ele invoca o analisador léxico. O analisador léxico processa o
fluxo de entrada e retorna o primeiro token que encontrar. Esse processo de requisição é contínuo
e só termina quando o analisador léxico identifica o fim o fluxo de entrada ou quando o analisador
sintático identifica uma falha gramatical.
3
Yacc: Yet Another Compiler Compiler
YACC (acrônimo para Yet Another Compiler Compiler) é um gerador de analisadores sintáticos desenvolvido por Stephen C. Johnson da ATT para o sistema operacional Unix. Ele gera um
8
9. analisador sintático, parte do compilador responsável por fornecer sentido sintático a um determinado código fonte, baseado numa gramática formal escrita numa forma similar ao formalismo
de Backus-Naur. O formalismo de gramáticas é mais poderoso que o de expressões regulares e
autômatos, assim é possível gerar programas que processam entradas mais complexas. O resultado
final após o uso do YACC é um código para o analisador sintático escrito em C.
O YACC costumava ser o gerador de analisadores sintáticos padrão na maioria dos sistemas
Unix, mas acabou sendo suplantado por versões mais modernas ainda que compatíveis, como
Berkeley Yacc, GNU bison, MKS yacc e Abraxas pcyacc. Uma versão atualizada do código original
da ATT é incluída no OpenSolaris. O YACC também já foi reescrito para outras linguagens,
incluindo Ratfor, EFL, ML, Ada, Java e Limbo.
O analisador sintático gerado pelo YACC requer um analisador léxico, que pode ser fornecido
externamente através de geradores de analisadores léxicos como o lex ou o flex. A norma POSIX
define a funcionalidade e os requisitos tanto para lex quanto para YACC.
Figura 7: Exemplificando a Integração Entre as Ferramentas Flex e Bison.
No ambiente unix existem outros pares que podem substituir o lex-YACC, sendo que a melhor
escolha recai no par flex-bison [Figura 7], em que o flex substitui o lex, enquanto o bison substitui
o YACC. Em particular, a implementação do scanner a partir do flex é muito superior à obtida
pelo lex, sendo totalmente compatível com o YACC. Assim, mesmo usando o YACC é preferível
que se use o flex. De qualquer forma, os dois pares aceitam os mesmos arquivos fonte para a
definição da linguagem, de modo que não é preciso escolher um deles a priori.
Da mesma forma que lex, YACC pode associar ações (trechos de programa) a cada regra da
gramática. À medida que a entrada é processada, ações adequadas são executadas. Essas ações
podem ser, por exemplo, a interpretação ou compilação da entrada.
3.1
Estrutura do Arquivo
O formato de um arquivo YACC é mais complexo do que o do lex. Ele também é composto
por partes de definição, regras e subrotinas, com uma sintaxe bastante diferenciada na seção de
regras, mesmo porque enquanto no lex a gramática é uma gramática regular, no yacc temos uma
gramática de atributos (que é uma gramática livre de contexto acrescida de atributos para a
manipulação semântica de contexto).
O problema na especificação da seção de regras de um arquivo yacc, que também é chamada de
corpo, é o fato de uma gramática exibir derivações bastante distintas a partir de um mesmo símbolo
não-terminal. Todas essas derivações devem aparecer no corpo, sendo que a ordem em que as
mesmas aparecem acaba influenciando a forma como o yacc determina as possíveis construções da
9
10. linguagem. Isso demanda um cuidado grande no momento de construir a gramática da linguagem,
caso não se queira perder um tempo considerável eliminando ambiguidades inexistentes.
3.2
Exemplo de um Arquivo YACC
O seguinte parser verifica a validade de uma lista de declarações para um determinado tipo
que é limitado a char, int e float, segundo o seu analisador léxico que é apresentado logo após sua
listagem.
%{
//---------------------------------------------------------------------// Parser que verifica se uma única linha de declaração é válida. Caso
// o parser encontre mais de um fechamento de declaração, isto é, mais
// de um símbolo ; então retornará um erro do tipo syntax error.
//---------------------------------------------------------------------#include stdio.h
extern char* yytext;
%}
%token CHAR COMMA FLOAT
%token ID INT SEMI
%%
decl : type ID list
{ printf([%s] t, yytext);
printf(Ponto e Virgula Alcançado! n); }
;
list : COMMA ID list { printf([%s] t, yytext); printf(COMMA ID list n); }
| SEMI
{ printf([%s] t, yytext); printf(SEMI n); }
;
type : INT
{ printf([%s] t, yytext); printf(INT
n); }
| CHAR
{ printf([%s] t, yytext); printf(CHAR
n); }
| FLOAT
{ printf([%s] t, yytext); printf(FLOAT n); }
;
%%
int main(void)
{
yyparse();
printf(Fim do Programa!n);
return 0;
}
// Função que trata erros oriundos da análise sintática de um trecho de
// código passado ao parser.
yyerror(char *s)
{
printf(Ocorreu o seguinte erro: %sn, s);
printf(Tal erro ocorreu na localidade próxima a: %sn, yytext);
}
O analisador léxico para tal parser é dado logo a seguir.
%{
#include stdio.h
#include stdlib.h
#include parser.tab.h
%}
10
11. id
[_a-zA-Z][_a-zA-Z0-9]*
wspc [ tn]+
semi [;]
comma [,]
%%
quit|exit { exit(EXIT_SUCCESS); }
int
{ return INT; }
char
{ return CHAR; }
float
{ return FLOAT; }
{comma}
{ return COMMA; }
{semi}
{ return SEMI; }
{id}
{ return ID;}
{wspc}
{}
3.3
Características
Abaixo são listadas algumas das características mais importantes da ferramenta YACC.
• As regras podem ser recursivas;
• As regras não podem ser ambíguas;
• Usa um parser bottom-up Shift/Reduce LALR
• Yacc não pode olhar mais que um token “lookahead”.
4
Conclusões
Através do pouco que vimos neste modesto relatório técnico podemos concluir, com um grande
fundo de certeza, que escrever compiladores não é uma tarefa fácil. Requer tempo e esforço.
Porém como a maioria das ações de um compilador são repetitivas, podemos empregar métodos
automáticos para construção de algumas de suas partes: o scanner e o parser. Atualmente, com
Figura 8: Partes de um Compilador.
a utilização de ferramentas como lex e YACC, é possível construir rapidamente um compilador.
O processo de geração automática utilizado por essas ferramentas, em geral, produz analisadores
quase tão rápidos e eficientes quanto os escritos de forma totalmente “artesanal”.
11
12. Referências
Aho, A. V., Sethi, R. Ullman, J. D. (1986), Compilers, Prinicples, Techniques and Tools,
Addison-Wesley, Massachusetts.
Appel, A. W. Ginsburg, M. (1998), Modern Compiler Implementation in C, 2 ed., Cambridge
University Press.
de Alencar Price, A. M. Toscani, S. S. (2004), Implementação de Linguagens de Progrmação:
Compiladores, number 9, 3 ed., Bookman.
Johnson, S. C. (1975), Yacc: Yet another compiler compiler, Computing Science Technical Report 32, Bell Laboratories, Murray hill, New Jersey.
Lesk, M. E. Schmidt, E. (1975), Lex - a lexical analyzer generator, Computing Science Technical
Report 39, Bell Laboratories, Murray Hill, New Jersey.
Levine, J. R., Mason, T. Brown, D. (1992), Lex Yacc, 2 ed., O’Reilly Associates.
Niemann, T. (2010), A Compact Guide to Lex Yacc. URL http://epaperpress.com/.
Sebesta, R. W. (2003), Conceitos de Linguagem de Programação, 5 ed., Bookman.
Stallman, R. Donnelly, C. (2010), ‘Bison, the yacc compatible parser generator’.
12