Lex e Yacc
Compiladores
Giovani Rubert Librelotto
[email protected]
Adaptação: Claudio Cesar de Sá
[email protected]
Roteiro

.... Há muita teoria... Até chegar neste
ponto...
Definição


Lex e Yacc são ferramentas que ajudam a criar
rotinas C para analisar e interpretar um arquivo de
entrada, na construção de um compilador
Eles podem ser usados para ajudar a escrever
compiladores e interpretadores, ou qualquer
outro programa cuja entrada pertence a uma
estrutura bem definida, e que deseja
transformar em outra.
Lex



Lê um arquivo contendo expressões regulares para
“pattern matching”
e gera rotinas C que
executam a análise léxica.
Essa rotina lê a entrada e identifica “tokens”.
Os “tokens” são definidos a partir de expressões
regulares (gramática regular, etc).
Yacc
Lê um arquivo que codifica a gramática de
uma linguagem e gera uma rotina de
“parsing”.
 Essa rotina agrupa os “tokens” em uma
seqüência significativa (de interesse), e
invocar rotinas para agir (realizar ações)
sobre elas.

Compiladores


Um compilador pega a descrição de um programa
e converte para um conjunto de instruções que
pode ser executadas por um computador.
A execução de um compilador tem 3 estágios:



Análise léxica - LEX
Análise sintática e semântica - YACC
Geração de Código.
Diagrama de um Compilador
Text File
usuário
Compiler
Progama
Executável
Etapas de um compilador
Cada estágio prepara a entrada para o
próximo estágio;
 Desse modo, o sistema operacional provém
um arquivo em bytes, a análise léxica
provém um agrupamento de tokens, o parser
provém um agrupamento de tokens em
instruções dentro da linguagem, e as ações
provém interpretação para os valores dos
tokens.

Modelo básico de processamento
para compiladores
input
Analisador
Léxico
Analisador
Sintático
(Parser)
Ações
Características Gerais






Lex gera código C para análise léxica.
Yacc gera código C para um parser.
Ambas especificações são menores que um
programa e mais fáceis de ler e entender.
Por definição, o sufixo de um arquivo Lex é .l e de
um arquivo Yacc é .y
Lex cria uma rotina chamada yylex em um
arquivo lex.yy.c
Yacc cria uma rotina chamada yyparse em um
arquivo chamado y.tab.c
Especif.
LEX
*.l
lex
Especif.
YACC
yacc
lex.yy.c
yylex( )
*.y
*.c
y.tab.c
yyparse( )
cc
program
Rotinas
C
Bibliot.
UNIX
libl.a
liby.a
Exemplo de Análise Léxica

Um programa que classifica uma entrada
em diversos tipos de tokens:
números;
 comandos;
 strings;
 comentários.

Programa em C (pags. 5 e 6)
 Especificação Lex (pag 7)

Expressões Regulares
Elas provém flexibilidade em muitas
situações.
 Ex.:



([0-9]*\.)*[0-9]+
Essa expressão permite reconhecer os
seguintes números:
100
 6.8
 45.92

Modelo de um autômato finito
[0 - 9]
[0 - 9]
1
0
. (ponto decimal)
. (ponto decimal)
2
[0 - 9]
[0 - 9]
3
Ações



A função básica de uma rotina léxica é retornar
tokens ao parser.
As ações, em uma especificação Lex, consiste em
expressões da linguagem C que retornam o token.
Para retornar o token em uma ação, usa-se a
expressão return.

[0-9]+
{
}
yylval = atoi(yytext);
return NUMBER
Um Parser




Um analisador léxico gera uma entrada para um
parser interpretar.
O parser organiza os tokens de acordo com as
regras de uma gramática.
A gramática, para uma linguagem de
programação, descreve a estrutura de entrada de
um programa.
Quando o parser tem uma seqüência de tokens,
que corresponde a uma regra, a ação associada é
executada.
Sumário de Rotinas Léxicas e
Sintáticas




A rotina main invoca (chama) yyparse para avaliar
se a entrada é válida.
yyparse invoca a rotina invoca a rotina yylex cada
vez que precisa de um token.
A rotina léxica lê da entrada, e a cada token
encontrado, retorna o número do token para o parser.
A rotina léxica pode também passar o valor do token
usando a variável externa yylval.
Fluxo de controle em rotinas léxicas
e sintáticas
main
Entrada
avaliada
Retorna 0 se
entrada é valida
1 se não
yyparse( )
Valor da
ação do
processo
yylval
Requerer
próximo token
Retornar o token
ou 0 se EOF
Ler chars
da entrada
yylex( )
Valor passado
do token
input
Os Mecanismos de Lex e Yacc
A partir de agora, o objetivo é ajudar a
entender como esses programas funcionam.
 E o que os programas geram.
 Veremos alguns programas simples.
 Será o básico do Lex e do Yacc.

LEX
1.1 Genericamente


É um gerador de programas (módulos) destinados
a processamento léxico de testos.
O módulo gerado pode funcionar como:





Filtro para processamento léxico;
“Pattern Matching” (expressões regulares);
Reconhecedor de linguagens regulares simples;
Pré-processador simples;
Analisador léxico para ligação de módulos de
reconhecimento sintático.
Usando o LEX

São três (quatro) etapas:
Escrever uma especificação Lex que contém
seqüências de caracteres em uma entrada. Esse
arquivo, por convenção, tem o sufixo .l.
 Executar a especificação Lex. Esse passo gera
um arquivo chamado lex.yy.c.
 Compilar lex.yy.c e qualquer arquivo fonte.






lex.yy.c não contém um programa completo.
Ele contém uma rotina léxica chamada yylex.
Você pode integrar o analisador léxico com um
parser gerado no Yacc.
Um programa léxico também assume a existência
de outras rotinas de suporte.
Você pode fornecer essas rotinas, ou usar as
bibliotecas standart do UNIX, a libl.a.
1.2 Funcionamento



Com base num texto “lex”, é gerado um módulo C
que contém um reconhecedor baseado em
autômatos determinísticos.
Este vai permitir encontrar patterns num texto de
entrada e sobre eles executar uma ação semântica
correspondente.
O módulo C é automaticamente escrito em um
arquivo “lex.yy.c” que inclui numa função
“yylex()”, a qual executa o algoritmo que se
apresenta:
FUNCAO yylex( ) : int
REPETIR
| yytext <- “ ”
| c <- input( )
| ENQUANTO (c cabe numa ExpRegular)
|
| yytext <- concatenar (yytext, c)
|
| c <- input( )
|
| executar a acãoC correspondente a ExpRegular
ATE chegar ao fim do input
yywrap( )
Escrevendo uma especificação
Lex




A principal seção de uma especificação léxica
contém um conjunto de regras.
A especificação mínima segue o formato:
%%
rules
%% indica o início da seção de regras.
Cada regra consiste em uma expressão regular.
1.3 Estrutura de um texto LEX

<definições gerais>
“%%”
<regras de tradução>
“%%”
<rotinas C auxiliares>
Cada regra vai ter a seguinte estrutura:
<EspRegular> “{“ <AçãoC> “}”
1.4 Um exemplo simples
%%
zippy
%%



printf(“Eu reconheci zippy”)
Este texto lex especifica um filtro para substituir
as ocorrências de “zippy” por “Eu reconheci ...”.
Todo o texto que não se “encaixe” em nenhum
pattern é simplesmente copiada pra saída.
Neste exemplo, tanto as definições gerais, como as
“rotinas C auxiliares” são inexistentes.
1.5 Utilização

cat sample.l
%%
zippy
%%


printf(“Eu reconheci zippy”)
lex sample.l
ls
lex.yy.c sample.l


cc -o sample lex.yy.c -ll
ls
lex.yy.c sample* sample.l

cat test
zippy
harry
zippy and zip

cat test | sample
Eu reconheci zippy
harry
Eu reconheci zippy and zip
Exemplo 2
%%
zip
printf(“ZIP”);
zippy
printf(“Eu reconheci zippy”);
zippy$
printf(“Achei zippy na final da linha”);
 Depois de refazer todas as etapas anteriores:
Achei zippy na final da linha
harry
Eu reconheci zippy and ZIP
Exemplo 3
%%
[A-Z]+ [ \t\n]
.
 cat test.caps
printf(“%s”, yytext);
;
Xview é uma API executando sob o X Window que suporta o
OPEN LOOK GUI.

caps < test.caps
API X
OPEN LOOK
Usando a especificação completa
<definições gerais>
“%%”
<regras de tradução>
“%%”
<rotinas C auxiliares>
Exemplo 4 - cutter.l
%%
int field_count = 1;
extern int field_cut;
\t
++field_count;
\n
{ECHO; field_count = 1; }
[^\t\n]+ {if(field_count == field_cut)
;
else
printf(“%s”, yytext); }
%%
int field_cut;
main(argc, argv)
int argc;
char *argv[ ];
{
if (argc > 1)
field_cut = atoi(argv[1]);
else
field_cut = 1;
yylex( );
}
lex cutter.l
 cutter 3 < test.cuts
 cc -o cutter lex.yy.c - abc def jkl
mno pqr
ll
wxy z
 cat test.cuts
 cutter < test.cuts
abc def ghi jkl

mno pqr stu
wxy z
def ghi jkl
pqr stu
z
1.5.1 Opções do comando LEX

-r 
ações em Fortran (RATFOR)

-t 
o arquivo lex.yy.c é enviado para
saída padrão

-v 
fornece estatísticas do autômato
gerado
1.6 Funcionamento como préprocessador

Para desenvolver um pré-processador, podese usar o módulo gerado através da função
correspondente ao seguinte algoritmo:
enquanto não fim input
yytext <- getSimbolo()
efetuar ação correspondente ao tipo de
símbolo encontrado
1.7 Funcionamento como
Analisador Léxico

Para funcionar como uma analisador léxico de um
compilador a ser invocado por um Parser, cada vez
que um símbolo é reconhecido, guarda-se o seu
valor (numa variável global) e retorna inteiro; para
tal, as regras terão o seguinte aspecto:
ExpReg
{ yylval <-- [guardar o valor do
símbolo];
return(código); }
YACC
Introdução
YACC é um comando UNIX.
 Seu significado é – Yet Another Compiler
Compiler
 O YACC recebe um texto com a descrição
de uma gramática com ações semânticas,
isto é, ações em C que indicam o que é que
se deve fazer cada vez que reconheceu uma
produção.





Ele gera um reconhecedor sintático do tipo
SLR(1) correspondente.
YACC é, portanto, um gerador de programas.
O texto de entrada pode incluir procedimentos
e/ou funções auxiliares.
O texto do reconhecedor gerado (código C) pode
ser unido a outros módulos e transportado para
outros ambientes.
Usando Yacc

São 4 passos para criar um parser:
Escrever uma especificação de uma gramática.
O arquivo terá a extensão .y.
 Escrever um analisador léxico que pode
produzir tokens, o yylex.
 Executar yacc na especificação para gerar o
código do parser, o y.tab.c.
 Compilar e linkar os códigos fontes para o
parser e o analisador léxico e rotinas auxiliares.






A saída y.tab.c contém uma rotina yyparse que
chama a rotina léxica yylex, para cada novo token.
Como Lex, Yacc não gera programas completos.
yyparse deve ser chamado de uma rotina main.
Um programa completo também requer uma rotina
de erro chamada yyerror chamada pelo yyparse.
Ambas, main e yyerror, podem ser supridas pelo
programador, através da biblioteca liby.a, que
pode ser linkada com a opção -ly, na compilação.
Escrevendo uma Especificação
Yacc

Uma especificação Yacc descreve uma gramática
livre do contexto que pode ser usada para gerar um
parser. Ela tem 4 classe de elementos:




Tokens, que é o conjunto de símbolos terminais;
Elementos sintáticos, que são os símbolos não terminais.
Regras de produção, que definem símbolos não terminais,
em termos de seqüência de terminais e não terminais;
Uma regra start, que reduz todos os elementos da
gramática para uma regra simples.
Foco de uma especificação Yacc



É um conjunto de regras de produções da forma:
símbolo:
definição
{ação}
;
Dois pontos (:) separam o lado esquerdo do lado
direito da regra, e o ponto-e-vírgula (;) finaliza a
regra.
Por convenção, a definição segue 2 “tabs” após os
dois pontos.
Símbolos




Cada regra em uma gramática é chamada por um
símbolo, um não terminal.
A definição consiste de zero ou mais terminais
(tokens ou caracteres literais) e outros não terminais.
Tokens são símbolos terminais reconhecidos pelo
analisador léxico, e só aparecem no lado direito da
regra.
Cada definição pode ter uma ação em C associada a
ela. Essa ação está entre chaves ( { } ).
Símbolos (continuação)





Os nomes de símbolos podem ter qualquer
tamanho, consistindo de letras, ponto, sublinhado
e números (exceto na primeira posição).
Caracteres maiúsculos e minúsculos são distintos.
Os nomes de não terminais são em minúsculos.
Tokens, em maiúsculos.
Uma especificação mínima Yacc consiste de uma
seção de regras precedidas por uma declaração dos
tokens usados na gramática.
Formato da especificação

O formato é similar ao formato do Lex.
declarações
%%
regras da gramática
%%
programas C
Exemplo 1 - print_int.y
%token INTEGER
%%
lines :
/* empty */
|
lines line
{
printf(“= %d\n”, $2); }
;
line :
INTEGER ‘\n’
{
$$ = $1; }
;
%%
#include “ lex.yy.c”
print_int




Especificação para gerar um programa que
imprime qualquer número recebido como entrada.
Na seção declaração, declaramos o token
INTEGER. Este símbolo é usado para a
comunicação entre o analisador léxico e o parser.
Na seção da gramática, especificamos lines e line.
A regra lines é definida para 0 ou mais linhas de
entrada.


A primeira alternativa é vazia.
A segunda alternativa é recursiva.
print_int.y - Ações das regras




Yacc provém pseudo-variáveis, que facilitam a as
ações de pegar o valor de um símbolo e setar o
valor retornado por uma ação.
O $ tem significado especial para o Yacc.
O valor de cada elemento pode ser obtido usando
a notação posicional: $1 para o 1º token, $2 para o
2º, e assim por diante.
O valor retornado pela ação é setado pela tarefa
para $$.
print_int.y - Ações das regras
line

:
{
Essa ação, “$1” retorna o token INTEGER. Há 2
elementos na definição, mas o ‘\n’ não retorna.
|
{

INTEGER ‘\n’
$$ = $1; }
lines line
printf(“= %d\n”, $2); }
Nessa ação, “$2” refere-se ao valor de line.
print_int.l - Analisador léxico
%%
[0-9]+
\n
[-+=]
quit
.
{
sscanf(yytext, “%d”, &yylval);
return(INTEGER);
}
return(‘\n’);
return yytext[0];
return 0;
;
print_int.y - #include



A terceira parte deste exemplo de especificação
Yacc contém a instrução #include.
Ela inclui o código para analisador léxico.
Para testar a especificação:
lex print_int.l
yacc print_int.y
cc -o print_int y.tab.c -ly -ll
print_int - Execução
$> print_int
3
=3
15
= 15
6
=6
zippy
sintax error
1.1 Gramática do YACC
textoYacc
->
<definições> “%%”
<produções> “%%”
<codigoCaux>
<definições>
->
<def>*
<produções>
->
<produção>+
1.1.1 Zona de Definições

Em <definições> são definidos os seguintes
elementos referentes a gramática:
Axioma da gramática;
 Conjunto dos terminais (T) e respectivo tipo de
valor, caso exista;
 Tipo de valor associados a cada não terminal
(NT), caso exista;


Ainda em <definições>, encontram-se
elementos referentes a resolução automática
de conflitos e ambigüidades:
Indicação de propriedades de operadores;
 Indicação de associatividade de operadores (à
esquerda, direita ou não associatividade).

1.1.1.1 Código C global
<def>
->
|
|
<defAxioma>
<defTipodeValor>
“%{“ <códigoCglobal> “%}”
|
<defTerminais>
|
<defAssociatividade>
|
<defTipodeValorT_NT>
<defAxioma> --> “%start” <unionDef>
<defTiposdeValor> -> “%union” <unionDef>
Definição de tipos de valores para T e NT.
 Esta definição será usada internamente para
a declaração de uma stack valor.
 <unionDef> é uma declaração de uma
union C;
 <unionDef> vai definir um conjunto de
identificadores de tipos que poderão ser
usados em <tipo>.

<defTerminais> -> “%token” <tipo> <T>*
Define os terminais da gramática podendo
ainda associar-lhes um tipo de valor.
 Essa definição pode ser feita em várias
linhas.

<tipo>
->
|
“<“ ID “>”
Tipo é opcional.
 Quando usado, ID é um identificador que
deve figurar na <unionDef> da
<defTipos_T_NT>

<defAssociatividade> -> “%left” <tipo> <T_NT>*
| “%rigth” <tipo> <T_NT>*
| “%noaccoc” <tipo> <T_NT>*



Indica se um operador é associativo ou não.
Esta informação é usada para tratamento de
conflitos. Nota-se, portanto, que podem ser
tratadas gramáticas ambíguas.
A cada símbolo S fica associada uma propriedade
de acordo com a ordem que ela aparece:


Os S declarados na mesma linha tem igual prioridade.
Os S declarados na linha i tem prioridade maior que os
da linha i-1.
<defTiposdeValorT_NT> -> “%type” <tipo> <NT>*

Permite associar um tipo de valor a um
terminal ou não terminal.
1.1.2 Produções
<produção> -> <NT> “:” <rhss>
Produção da gramática segundo a estrutura
sintática habitual.
 <NT> = não terminal
 rhs = rigth hand side – lado direito da produção

<rhss>

->
|
<rhs>
<rhs> “|” <rhss>
Lado direito possivelmente múltiplo, e
nesse caso, separa-se por “|”
<rhs>

->
|
<simb>*
<simb>* <prec>
Lado esquerdo é uma seqüência de
símbolos podendo opcionalmente incluir
<prec> para alternar propriedades de
operadores.
<simb>
->
|
|
<T>
<NT>
<simbSemântico>
T = símbolo terminal
 NT = símbolo não terminal
 simbSemantico = Símbolo semântico, para
colocar ações no parser.

<T> ->
|

ID_token
“ ’ ” char “ ‘ “
T – símbolo terminal é um identificador
declarado como token ou caracter entre
aspas.
<NT>

->
ID
O identificador ID deve figurar, em pelo
menos, um lado esquerdo de uma produção.
<simbSemantico> -> “{“ <codigoCcom$> “}”




<codigoCcom$> -> código C com possibilidade
de usar $$, $1, ...;
$$ permite a associação de um valor ao lado
esquerdo da produção ($$ = .. – atributo
sintetizado)
$i permite a utilização de um valor de um símbolo
do rhs (i – na ordem do símbolo)
Esse símbolo pode ser T, NT ou simbSemantico.
<prec>

->
“%prec” <T>
Permite associar a prioridade de um
símbolo terminal T a uma produção.
1.1.3 Código C
<codigoCaux> ->
|
<funcoes_C>
“%%” <funcoes_C>
/* vazio*/
-> código C puro;
Não esquecer a definição da
função de análise léxica
yylex()
Ex: #include “lex.yy.c”
Exemplo: Calculadora
Operações da Calculadora

Soma:


3+9
Subtração:

36.4 - 95
Análise

Léxica:
36.7 + 43.2
 REAL PLUS REAL


Sintática:
rexpr <-- REAL | rexpr ‘+’ rexpr
{$$ = $1 + $3;}
 line <-- ‘\n’ | rexpr ‘\n’

Expressões regulares para tokens

Para números inteiros:


Para números reais:


[0-9]+
[0-9]*\”.”[0-9]+
Para números reais com expoentes:

([0-9]*\”.”[0-9]+) | ([0-9]*\”.”[0-9]+ [eE] [+-]? [0-9]+)
Falta o exemplo da calculadora.
Está no texto em inglês, no
arquivo em PDF e no primeiro
link do site!
Download

Lex e Yacc