Lex e Yacc Compiladores Giovani Rubert Librelotto [email protected] Adaptação: Claudio Cesar...
-
Upload
luiza-almada-ferretti -
Category
Documents
-
view
221 -
download
0
Transcript of Lex e Yacc Compiladores Giovani Rubert Librelotto [email protected] Adaptação: Claudio Cesar...
Lex e Yacc
CompiladoresCompiladoresGiovani Rubert Librelotto
[email protected]ção: Claudio Cesar de Sá
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 CompilerProgama
Executável
usuário
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
AnalisadorSintático(Parser)
input AnalisadorLéxico
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
Bibliot.UNIX
RotinasCyylex( ) yyparse( )
lex
cc
program
yacc
Especif.YACC
*.l *.y
lex.yy.c y.tab.c *.c
libl.aliby.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
01
2
3
[0 - 9]
[0 - 9]
[0 - 9]
. (ponto decimal)
. (ponto decimal)[0 - 9]
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
yyparse( )
yylex( )yylval
input
Entradaavaliada
Valor da ação do processo
Requererpróximo token
Retornar o tokenou 0 se EOF
Valor passadodo token
Ler charsda entrada
Retorna 0 se entrada é valida1 se não
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( ) : intREPETIR | yytext <- “ ”| c <- input( )| ENQUANTO (c cabe numa ExpRegular)| | yytext <- concatenar (yytext, c)| | c <- input( )|| executar a acãoC correspondente a ExpRegularATE chegar ao fim do inputyywrap( )
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 lslex.yy.c sample.l cc -o sample lex.yy.c -ll lslex.yy.c sample* sample.l
cat testzippyharryzippy and zip cat test | sampleEu reconheci zippyharryEu 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 linhaharryEu reconheci zippy and ZIP
Exemplo 3
%%[A-Z]+ [ \t\n] printf(“%s”, yytext);. ; cat test.capsXview é uma API executando sob o X Window que suporta o
OPEN LOOK GUI. caps < test.capsAPI XOPEN 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]);elsefield_cut = 1;yylex( );
}
lex cutter.l cc -o cutter lex.yy.c
-ll cat test.cutsabc def ghi jklmno pqr stuwxy z
cutter 3 < test.cutsabc def jklmno pqr wxy z cutter < test.cutsdef ghi jklpqr stuz
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, pode-
se usar o módulo gerado através da função correspondente ao seguinte algoritmo:
enquanto não fim inputyytext <- 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 : INTEGER ‘\n’{ $$ = $1; }
Essa ação, “$1” retorna o token INTEGER. Há 2 elementos na definição, mas o ‘\n’ não retorna.
| lines line{ printf(“= %d\n”, $2); }
Nessa ação, “$2” refere-se ao valor de line.
print_int.l - Analisador léxico
%%[0-9]+ {
sscanf(yytext, “%d”, &yylval);return(INTEGER);}
\n return(‘\n’);[-+=]return yytext[0];quit 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.lyacc print_int.ycc -o print_int y.tab.c -ly -ll
print_int - Execução
$> print_int3=315= 156= 6zippysintax error
1.1 Gramática do YACCtextoYacc -> <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>| /* vazio*/
<funcoes_C> -> 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: [0-9]+
Para números reais: [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 primeirolink do site!