Lex e Yacc Compiladores Giovani Rubert Librelotto giovanir@inf.ufrgs.br Adaptação: Claudio Cesar...

Preview:

Citation preview

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!

Recommended