Você está na página 1de 24

COMPILADORES 1

ÚNICA – Faculdade Única de Ipatinga


Curso: Ciência da Computação
Disciplina: COMPILADORES
Prof.: Filipe C. Fernandes

Objetivos:

 Apresentar as principais etapas envolvidas no processo de compilação;


 Conhecer as fases de projeto de um compilador;
 Estudar a análise léxica do código-fonte;
 Estudar a análise sintática;
 Estudar as principais características da análise semântica, geração e
otimização de código.

Bibliografia:

 Aho, V. A., Sethi, R., Ullman, D. J. Compiladores – Princípios, técnicas e


ferramentas. Livros Técnicos e Ciêntíficos Editora, 1995.
COMPILADORES 2

Sumário

1 INTRODUÇÃO .................................................................................................................... 3
2 O MODELO DE COMPILAÇÃO DE ANÁLISE E SÍNTESE ......................................................... 4
3 O CONTEXTO DE UM COMPILADOR ................................................................................... 5
4 ANÁLISE LÉXICA ................................................................................................................. 6
5 ANÁLISE SINTÁTICA E SEMÂNTICA ..................................................................................... 7
5.1 As fases da análise .....................................................................................................8
6 TRADUÇÃO DIRIGIDA PELA SINTAXE – Análise semântica .................................................. 9
6.1 Notação pósfixa ......................................................................................................... 9
6.2 Análise semântica (continuação).............................................................................. 10
6.3 Tabela de Símbolos .................................................................................................. 10
6.4 Geração de Código .................................................................................................. 10
6.5 Ferramentas utilizadas na construção de um compilador ......................................... 10
6.6 Definições ................................................................................................................ 10
7 ÁRVORES GRAMATICAIS .................................................................................................. 12
8 EXPRESSÕES REGULARES ................................................................................................. 13
8.1 Definições formais para expressões regulares .......................................................... 13
9 ANÁLISE GRAMATICAL..................................................................................................... 15
9.1 Análise Gramatical Top-Down .................................................................................. 15
9.2 Recursividade à Esquerda ........................................................................................ 16
9.2.1 Eliminação da recursividade à Esquerda ........................................................... 17
10 GRAMÁTICA LIVRE DE CONTEXTO ................................................................................ 18
11 LEX E YACC (Flex e Bison) ............................................................................................. 19
11.1 Primeira parte do arquivo Lex: Declarações ............................................................. 19
11.2 Segunda parte do arquivo Lex: Regras de tradução .................................................. 19
11.3 Terceira parte do arquivo Lex: Procedimentos Auxiliares ......................................... 20
11.4 Exemplo Lex e Yacc .................................................................................................. 22
12 TABELAS DE SÍMBOLOS................................................................................................ 24
12.1 Entradas da Tabela de Símbolos............................................................................... 24
COMPILADORES 3

1 INTRODUÇÃO

Compiladores são programas de computador que traduzem de uma linguagem


para outra. Um compilador recebe como entrada um programa escrito na linguagem-
fonte e produz um programa equivalente na linguagem –alvo. Geralmente, a linguagem-
fonte é uma linguagem de alto nível, com C ou C++, e a linguagem-alvo é um código-
objeto (algumas vezes também chamado de código de máquina) para a máquina-alvo,
ou seja, um código escrito usando as instruções de máquina do computador, no qual ele
será executado. Podemos ver esse processo esquematicamente assim:

Figura 1 - Esquema de um compilador


COMPILADORES 4

2 O MODELO DE COMPILAÇÃO DE ANÁLISE E SÍNTESE

Existem duas partes na compilação: a análise e a síntese. A parte de análise divide


o programa fonte nas partes constituintes e cria uma representação intermediária do
mesmo. A de síntese constrói o programa alvo desejado, a partir da representação
intermediária.
Durante a análise, as operações implicadas pelo programa fonte são determinadas
e registradas numa estrutura hierárquica, chamada de árvore. Frequentemente, é
utilizado um tipo especial de árvore, chamado árvore sintática, na qual cada nó
representa uma operação e o filho de um nó representa o argumento da operação. Por
exemplo, a árvore sintática para um enunciado de atribuição é mostrada a seguir:

Figura 2 - Árvore sintática para montante := depósito_inicial + taxa_de_juros * 60


COMPILADORES 5

3 O CONTEXTO DE UM COMPILADOR

Adicionalmente ao compilador, vários outros programas podem ser necessários


para se criar um programa alvo executável. Um programa fonte pode ser dividido em
módulos armazenados em arquivos separados. A tarefa de coletar esses módulos é
algumas vezes, confiada a um programa distinto, chamado de pré-processador. O pré-
processador pode, também, expandir formas curtas, chamadas de macros, em
enunciados da linguagem fonte.
A Figura 3 mostra uma “compilação” típica. O programa alvo criado pelo
compilador pode exigir processamento posterior antes que possa ser executado. O
compilador cria um código de montagem que é traduzido no de máquina por uma
montador e, então, ligado a algumas rotinas de biblioteca, formando o código que é
efetivamente executado em máquina.

Figura 3 - Sistema de processamento de linguagem


COMPILADORES 6

4 ANÁLISE LÉXICA

Num compilador, a análise linear é chamada de análise léxica ou


esquadrinhamento (scanning). Por exemplo, na análise léxica, os caracteres no
enunciado de atribuição: montante := depósito_inicial + taxa_de_juros * 60, poderiam
ser agrupados nos seguintes tokens:

 O identificador: montante
 O símbolo de atribuição: :=
 O identificador: deposito_inicial
 O sinal de adição
 O identificador: taxa_de_juros
 O sinal de multiplicação
 O número: 60

Os espaços que separam os caracteres desses tokens seriam normalmente


eliminados durante a análise léxica.
COMPILADORES 7

5 ANÁLISE SINTÁTICA E SEMÂNTICA

A fase de análise sintática tem por função verificar se a estrutura gramatical do


programa está correta (isto é, se essa estrutura foi formada usando as regras gramaticais
da linguagem). A análise semântica tem por função verificar se as estruturas do
programa irão fazer sentido durante a excução.
O analisador sintático identifica sequências de símbolos que constituem estruturas
sintáticas (por exemplo expressões e comandos), através de uma varredura ou parsing
da representação interna (sequência de tokens) do programa fonte. O analisador
sintático produz uma estrutura em árvore, chamada árvore de derivação, que exibe a
estrutura sintática do texto fonte, resultante da aplicação das regras gramaticais da
linguagem. Em geral, a árvore de derivação não é produzida explicitamente, mas fica
implícita nas chamadas das rotinas recursivas que executam a análise sintática. Em
muitos compiladores, a representação interna do programa resultante da análise sintática
é uma árvore de derivação compactada, denominada árvore de sintaxe, que elimina
redundâncias e elementos supérfluos. Essa estrutura objetiva facilitar a geração do
código que é a faze seguinte à análise.
Outra função dos reconhecedores sintáticos é a detecção de erros de sintaxe
identificando clara e objetivamente a posição e o tipo de erro ocorrido. Mesmo que
erros tenham sido encontrados, o analisador sintático deve tentar recuperá-lo
prosseguindo a análise do texto restante.
Muitas vezes, o analisador sintático opera conjuntamente com o analisador
semântico, cuja principal atividade é determinar se as estruturas sintáticas analisadas
fazem sentido ou não. Por exemplo, o analisador semântico verifica se um identificador
declarado como variável é usado como tal; se existe compatibilidade entre operandos e
operadores em expressões; etc.
Para exemplificar, considere o seguinte comando pascal:
while <expressão> do <comando>;

Neste caso, as estrutura <expressão> deve apresentar-se sintaticamente correta, e


sua avaliação deve retornar um valor do tipo lógico. Isto é, a aplicação de operadores
(relacionais/lógicos) sobre os operandos (constantes/variáveis) deve resultar num valor
do tipo lógico (verdadeiro/falso).
As regras gramaticais que definem as construções da linguagem podem ser
descritas através de produções (regras que produzem, geram) cujos elementos incluem
símbolos terminais (aqueles que fazem parte do código-fonte) e símbolos não-terminais
(aqueles que geram outras regras).
COMPILADORES 8

5.1 As fases da análise

Figura 4 - Fases da análise


COMPILADORES 9

6 TRADUÇÃO DIRIGIDA PELA SINTAXE – Análise semântica

É usada para associar informações às construções de linguagens. O que deve


acontecer quando o comando write(a) for executado?
Tradução Dirigida pela Sintaxe é utilizada principalmente para:

 Especificar a verificação semântica;


 Verificação e instalação de tipos;
 Verificação da corretude do programa sendo compilado com relação às
regras da linguagem que não podem ser descritas na gramática;
 Ex.: em uma atribuição b = expr
 A variável b deve estar declarada, ou instalada na tabela de
símbolos, o tipo de expr deve ser diferente de void e convertível
para o tipo de b.
 O número de parâmetros reais de uma chamada de função deve ser igual
ao número de parâmetros formais da função, e o tipo de cada parâmetro
deve ser compatível com o parâmetro formal correspondente;
 Usado para geração de código intermediário;
 Emitir mensagens de erro;

A tradução dirigida pela sintaxe associa informações às construções sintáticas de


uma linguagem do seguinte modo:
 Cada símbolo terminal e não-terminal de uma produção é associado a um
conjunto de atributos (tipo, endereço, de memória, valor, etc.);
 A uma determinada produção é associada um conjunto de ações
semânticas que indicam como os valores dos atributos dos terminais e não-
terminais podem ser obtidos;

6.1 Notação pósfixa

A notação pósfixa para uma expressão E pode ser definida indutivamente como se
segue:
1. Se E for uma variável ou uma constante, então a notação pósfixa para E
será o próprio E.
2. Se E for uma expressão da forma E1 op E2, onde op é qualquer operador
binário, então a forma pósfixa para E será E1E2op.

Os parênteses não são necessários na notação pósfixa porque a posição e a aridade


(número de argumentos) dos operadores permitem somente uma única decodificação de
uma expressão pósfixa. Por exemplo, a notação pósfixa para (9-5)+2 é 95-2+ e a
notação pósfixa para 9-(5+2) é 952+-.
COMPILADORES 10

Exercício

1. Elabore um programa em C para ler uma expressão infixa e escrever sua


forma pósfixa.

6.2 Análise semântica (continuação)

 Verificar por erros semânticos, com base nos tipos de tokens;


 Verificação para assegurar que os componentes de um programa se
combinam de forma significativa;
 Avalia o programa fonte sob os demais aspectos que não estejam
relacionados ao aspecto léxico ou sintático.

6.3 Tabela de Símbolos

 Guarda informações sobre identificadores usados no programa-fonte;


 Uma tabela de símbolos é uma estrutura de dados contendo um registro
para cada identificador, com campos para seus atributos;
 Quando o analisador léxico detecta um identificador, instala-o na tabela de
símbolos.

6.4 Geração de Código

 Gera código de máquina realocável ou código de montagem;

6.5 Ferramentas utilizadas na construção de um compilador

 Lex ou Flex: Scanner Generator. Gera scanners léxicos baseados na


descrição de tokens válidos (analisador léxico).
 Yacc ou Bison: Parser Generator. Gera parser baseados em uma gramática
descrevendo a sintaxe da linguagem (analisador sintático).

6.6 Definições

 Um token é formado por (lexema, padrão ou classe);


 Lexema: Caracteres específicos de uma código fonte que seguem um
determinado padrão;
 Padrão: Descreve um conjunto de strings que pertencem a uma
determinado tipo de token;
COMPILADORES 11

CLASSE LEXEMA
identificador abc, x, y
if if
num 10, 9.5
op. relacionais <=, >
atrib =, :=
COMPILADORES 12

7 ÁRVORES GRAMATICAIS

Uma árvore gramatical mostra, como o símbolo de partida de uma gramática


deriva uma cadeia de linguagem. Se um não-terminal A possui uma produção A 
XYZ, então uma árvore gramatical pode ter um nó interior rotulado A, com três filhos
rotulados X, Y, Z da esquerda para a direita:
Formalmente, dada uma gramática livre de contexto (GLC), uma árvore
gramatical possui as seguinte propriedades:
 A raiz é rotulada pelo símbolo de partida;
 Cada nó interior é rotulado por um token ou por ε (vazio);
Cada nó interior é rotulado por uma não-terminal.
 Se A é um não-terminal rotulando algum nó interior e X1, X2, ..., Xn são os
rótulos dos filhos daquele nó, da esquerda para a direita, então A 
X1X2...Xn é uma produção.
Aqui X1, X2, ..., Xn figuram no lugar de símbolos que sejam terminais ou não-
terminais. Como um caso especial, se A  ε (vazio), então um nó rotulado A deve
possuir um único filho rotulado ε.
Ex.: Para 9-5+2, temos as produções:
lista  lista + dígito
lista  lista – dígito
lista  dígito
dígito  0|1|2|3|4|5|6|7|8|9
e a árvore gramatical:

Figura 5 - Árvore gramática para 9-5+2


COMPILADORES 13

8 EXPRESSÕES REGULARES

As expressões regulares (ER) são uma notação importante para a especificação de


padrões. Cada padrão corresponde a um conjunto de cadeias de caracteres e, dessa
forma, as expressões regulares servirão para respresentar conjuntos de cadeias de
entrada.
As ERs permitem escrever certos tipos de linguagens chamadas linguagens
regulares.
Operações regulares permitem a construção de expressões regulares pela
combinação de linguagens.

8.1 Definições formais para expressões regulares

Alfabeto ( ∑ ) denota qualquer conjunto finito de símbolos. O conjunto {0,1}


representa alfabeto ( ∑ ) binário, ascii, de símbolos.
λ é uma expressão regular denotada por { λ }, ou seja, a linguagem que existe na
string vazia.
Se a pertence ao ∑, então a é uma expressão regular denotando a linguagem {a}.
Suponha que R e S são ERs denotando as linguagens L(R) e L(S).
a) (R)●(S) denota a linguagem L(R) ● L(S). Concatenação: uma string s pertence a
(R)●(S) se ela é a concatenação de duas strings α e β onde α ϵ L(R) e β ϵ L(S).
b) (R)U(S) denota a linguagem L(R) U L(S). União: uma string s pertence a
(R)U(S) se ela está em L(R) ou em L(S).
c) (R)* fechamento Kleene Star: denota a linguagem obtida pela concatenação de
zero ou mais strings de L(R).
+
d) (R) fechamento Kleen Positivo: denota a linguagem obtida pela concatenação
de uma ou mais strings de L(R).

Linguagem é um conjunto de strings formados por elementos de um ∑ e possui as


seguintes operações:

União: U
Concatenação: ●
Fechamento Kleene Star: *
Fechamento Kleene Positivo: +

Exercícios

1) Seja L1 = {λ}, L2 = {a,b} e L3 = {c}, determine:

a) L2 U L3
b) L1 U L2 U L3
COMPILADORES 14

c) L2●L3
d) L2*
+
e) L2

2) Diga o que pode ser reconhecido pelas seguintes expressões:

a) a (a* | b*) b
b) (c* (a | ( bc* ))*)
c) (a | b)* a ( a | b )*
+
d) ((( a* a ) b ) | b )
e) [abcd]
f) [c-h]
g) [a-zA-Z]

1) Defina uma ER que descreva:

a) Um a seguido de qualquer quantidade de b.


b) Qualquer palavra sobre a e b com pelo menos 3 a´s consecutivos.
c) Qualquer palavra sobre ∑ = {0,1} terminada em 01.
d) Qualquer palavra sobre ∑ = {0,1} que não termina em 01 e 10.
e) Para representar um número com casas decimais (float).
f) Para representar um identificador (variável)
COMPILADORES 15

9 ANÁLISE GRAMATICAL

A análise gramatical é o processo de se determinar se uma cadeia de tokens pode


ser gerada por uma gramática ou não. Na discussão do problema, serve como apoio ao
raciocínio pensar que uma árvore gramatical está sendo construída, ainda que um
compilador não a construa efetivamente. No entanto, um analisador gramatical precisa
ser capaz de construir uma árvore ou então a compilação não poderá ser garantida
correta.
A maioria dos métodos de análise gramatical cai em uma dentre duas classes,
chamadas de top-down e botton-up. Esses termos se referem à ordem na qual os nós da
árvore gramatical são construídos. No primeiro, a construção se inicia na raiz e
prossegue em direção às folhas, enquanto que no último, a construção se inicia nas
folhas e procede em direção à raiz.

9.1 Análise Gramatical Top-Down

A seguinte gramática gera um subconjunto dos tipos de pascal. O token


pontoponto é usado para enfatizar que a sequência de caracteres é tratada como uma
unidade.

tipo  tipo_simples | array [ tipo_simples ] of tipo

tipo_simples  integer | char | num pontoponto num

A construção top-down de uma árvore gramatical é feita iniciando-se pela raiz,


rotulada pelo não-terminal de partida e realizando-se, repetidamente, os dois seguintes
passos:

1. Ao nó n, rotulado por um não-terminal A, selecione uma das produções


para A e construa os filhos de n com os símbolos no lado direito da
produção.

2. Encontre o próximo nó no qual uma subárvore deve ser construída.

Para algumas gramáticas, os passos acima podem ser implementado durante um


único esquadrinhamento da cadeia de entrada, da esquerda para a direita. O token
correntemente esquadrinhado à entrada é frequentemente referenciado como o símbolo
lookAHead. Inicialmente o símbolo lookahead é o primeiro, isto é, é o token mais à
esquerda da cadeia de entrada.
COMPILADORES 16

Procedimento reconhecer(t:token); tipo;


Início fim
se lookahead = t então senão
lookahead := próximo_token erro;
senão fim;
erro;
fim; procedimento tipo_simples;
início
procedimento tipo; se lookahead=integer então
início reconhecer(integer);
se lookahead está em {integer, char, num} senão
então se lookahead=char então
tipo_simples; reconhecer(char);
senão senão
se lookahead = “id” então se lookahead=num então
reconhecer(id); início
senão reconhecer(num);
se lookahead = array então reconhecer(pontoponto);
início reconhecer(num);
reconhecer(array); fim
reconhecer(‘[‘); senão
tipo_simples; erro;
reconhecer(‘]’); fim;
reconhecer(of);

Pseudocódigo para um analisador gramatical preditivo

9.2 Recursividade à Esquerda

É possível um analisador gramatical descendente recursivo rodar infinitamente


para determinados casos. O problema emerge em produções recursivas à esquerda, tais
como:
expr  expr + termo

Quando o símbolo mais à esquerda do lado direito da produção é o mesmo que o


não-terminal no lado esquerdo da produção. Suponhamos que o procedimento para expr
decida aplicar esta produção. O lado direito da produção começa com expr, de tal forma
que o procedimento expr é chamado recursivamente e o analisador roda para sempre.
Note que o símbolo lookahead muda somente quando um terminal do lado direito é
reconhecido . Como a produção começa pelo não-terminal expr, nenhuma mudança à
entrada tem lugar entre as chamadas recursivas, causando um laço infinito.
Uma produção recursiva à esquerda pode ser eliminada pela reescrita da produção
culpada. Considere um não-terminal A com duas produções:

A  Aα | β
COMPILADORES 17

onde α e β são sequências de terminais e não-terminais que não começam por A. Por
exemplo, em:
EE+T|T
(A = E; α = +T e β = T)
O não-terminal A é recursivo à esquerda porque a produção A  Aα possui o
próprio A como seu símbolo mais à esquerda, no lado direito. A aplicação repetida
dessa produção constrói uma sequência de α´s à direita de A.

9.2.1 Eliminação da recursividade à Esquerda

1) E  E + T | T 2) B  B * A | T

E  TX B  TX
X  +TX | λ X  *AX | λ

Devemos criar um novo não-terminal qualquer, que vamos chamar aqui de X. A


produção X  αX é recursva à direita porque a produção para X possui o próprio X
como o ultimo símbolo no lado direito.
COMPILADORES 18

10 GRAMÁTICA LIVRE DE CONTEXTO

Uma gramática G é GLC (Gramática Livre de Contexto) se obedece a conversão


da produção vazia (Sλ), e para qualquer produção (αβ) exceto (Sλ), α é uma
única variável não-terminal, ficando β livre, podendo aparecer qualquer combinação de
variáveis não-terminais e terminais.

S λ
S a
S (S)

Este tipo de gramática pode representar praticamente todas as necessidades para


construção de uma linguagem de programação.

Exercício: Elabore uma GLC para reconhecer, especificamente, programas


pascal com as seguintes características:

program teste
var
x, y : integer;
z : char;
begin
x := 10;
y := x;
if (x >= 10) then
begin
write(“teste”);
end;
end.
COMPILADORES 19

11 LEX E YACC (Flex e Bison)

Lex: O Lex é um gerador de analisador léxico. O seu propósito é transformar uma


série de símbolos em tokens. O mesmo recebe uma especificação num arquivo de
extensão .l, contendo o conjunto de expressões regulares que deve reconhecer as ações
associadas a cada token reconhecido.
Um programa Lex é dividido em três partes separadas pelos símbolos %%
declarações

%%

regras de tradução

%%

procedimentos auxiliares

11.1 Primeira parte do arquivo Lex: Declarações


 Na seção de declarações são feitas as declarações de variáveis, definições
e inclusões de arquivos entre %{ e %}.
 Podemos definir expressões regulares para notações não-terminais
 Esta notação pode acontecer tanto na primeira parte do arquivo quanto na
segunda, desde que seja feita entre { e }.

Exemplo:
%{
#include “calc.h”
#include <stdio.h>
#include <stdlib.h>
%}
/*Expressões regulares*/
branco [\t\n ]+
letra [A-Za-z]
digito [0-9]
identificador {letra}( _ | {letra} | {digito} )*

11.2 Segunda parte do arquivo Lex: Regras de tradução


 Alguma especificação pode ser escrita nesta parte do programa entre %{ e
%}. Esta especificação será colocada no início da função yylex(), que é a
função que reconhece os tokens e retorna um número inteiro.
COMPILADORES 20

 Regras de tradução tem a sintaxe exp_regular ação, se a ação não for


especificada o token reconhecido será utilizado. Se a ação tiver mais de
uma instrução ou for utilizada maus de uma linha deve estar entre { e }.
 A variável yytext armazena os tokens reconhecidos pelas expressões
regulares. Esta variável é composta de uma tabela de caracteres com o
comprimento armazenado na variável yyleng.

11.3 Terceira parte do arquivo Lex: Procedimentos Auxiliares

 Aqui será inserido código de programação necessário pra suprir suas


necessidades de programação. Se nada for inserido, o Lex considera o
código abaixo:

main()
{
yylex();
}

Para compilar:

_ lex ex1.l

_ gcc –o ex1.out lex.yy.c –lfl (éle éfe éle)


ou
_ cc lex.yy.c –o ex1 –ll (éle éle)

Exercício Prático

%{
int nline;
%}

%%

fic printf(“\nFaculdades Integradas de Caratinga”);


cc printf(“\nCurso de Ciência da Computação”);
. printf(“\nErro: entrada = %s”, yytext);
\n { ++nline; printf(“\nNova Linha”); }

%%

int main()
COMPILADORES 21

{
yylex();
printf(“\nFim de programa de %d linhas”,nline);
}

O programa Lex gera o que é chamado de “Lexer”. Isto é, uma função que pega
uma stream de caracteres como foi entrada, e sempre que ela vê um grupo de caracteres
que casa um padrão, realiza uma determinada ação. Um exemplo bem simples:

%{
#include<stdio.h>
%}

%%

stop printf(“\nStop command received.”);


start printf(“\nStart command received.”);

Sempre que ‘Stop’ é encontrado na entrada, o resto da linha (uma chamada


printf()) é executada. Abaixo de ‘Stop’, temos também definido ‘Start’, que faz a
mesma coisa.
O próximo exemplo mostra como usar expressões regulares no Lex:

%{
#include<stdio.h>
%}

%%

[0-9]+ printf(“\nNúmero”);
[a-zA-Z][_a-zA-Z0-9]* printf(“\nIdentificador”);

Yacc: O Yacc é um gerador de analisador sintático para Gramáticas Livre de


Contexto – GLC. O Yacc recebe um texto com a descrição de uma gramática com ações
semânticas e gera um reconhecedor sintático correspondente.

Para compilar:

_ yacc –d ex4.y
_ lex ex4.l
_ gcc –o ex4.out y.tab.c –lfl (éle éfe éle)

O Yacc pode fazer o “parsing” da entrada de streams dotadas de tokens com


certos valores. Isto claramente descreve a relação que o Yacc tem com o Lex. Yacc não
COMPILADORES 22

faz idéia do que são “streams de entrada”, ele precisa de tokens preprocessados. Para
auxiliar no trabalho do Yacc, podemos escrever nosso próprio “tokenizer” ou usar o Lex.

11.4 Exemplo Lex e Yacc

/*ARQUIVO: ex3.l - Especificação (F)Lex para subconjunto Pascal */

%{
#include “y.tab.h”
%}
ID [A-Za-z][A-Za-z0-9_]*

%%

“begin” { return Tbegin; }


“end” { return Tend; }
“:=” { return Tassign; }
{ID} {return Tidentifier; }
[0-9]+ {return Tnumber; }
[ \n\t] /* ignora espaços em branco */
. { return *yytext; } /* o ponto quer dizer qq outro caracter que ainda não
foi descrito como um token */

%%

/* ARQUIVO: ex3.y – Especificação Yacc para subconjunto Pascal */

%{
#include<stdio.h>
%}

%token Tbegin
%token Tend
%token Tassign
%token Tidentifier
%token Tnumber
%left ‘+’
%left ‘-’

%start inicio

%%
COMPILADORES 23

inicio : Tbegin stt Tend ;


stt : statement ‘.’ | statement ‘;’ stt ;
statement : Tidentifier Tassign expression | ;
expression : Tnumber | Tidentifier | expression ‘+’ expression | expression ‘-’
expression ;

%%

int main()
{
yyparse();
}
int yyerror(char *s)
{
fprintf(stderr, “%s \n”, s);
}
#include “lex.yy.c”
COMPILADORES 24

12 TABELAS DE SÍMBOLOS

Um compilador usa uma tabela de símbolos para controlar as informações de


escopo e das amarrações a respeito dos nomes. A tabela de símbolos é pesquisada a
cada vez que um nome é encontrado no texto-fonte. As mudanças na tabela ocorrem se
um novo nome ou uma nova informação a respeito de um nome já existente for
descoberta.
Um mecanismo de tabela de símbolos precisa permitir que adicionemos novas
entradas e encontremos eficientemente as já existentes. Os dois mecanismos de tabelas
de símbolos mais utilizados são as listas lineares e as tabelas hash. Avaliamos cada
esquema na base do tempo requerido para adicionar N entradas e realizar E inquisições.
Uma lista linear é o mais simples de implementar, mas seu desempenho é pobre quando
E e N se tornam grandes. Os esquemas de hashing providenciam um melhor
desempenho para um esforço de programação um tanto maior e uma sobrecarga de
espaço também maior.
É útil para um compilador ser capaz de crescer a tabela de símbolos
dinamicamente, se necessário, em tempo de compilação. Se o tamanho da tabela de
símbolos for fixado quando o compilador for escrito, o mesmo deve ser escolhido
grande o suficiente para tratar qualquer programa-fonte que lhe seja apresentado. Tal
tamanho fixo está inclinado a ser grande para a maioria dos programas e, por outro lado,
inadequado para alguns.

12.1 Entradas da Tabela de Símbolos

Cada entrada da tabela de símbolos é destinada à declaração de um nome. O


formato das entradas não tem que ser uniforme, porque a informação guardada a
respeito de um nome depende do uso do mesmo. Cada entrada pode ser implementada
como um registro consistindo em uma sequência de palavras consecutivas na memória.
Para manter a tabela de símbolos uniforme, pode ser conveniente que algumas das
informações a respeito de um nome sejam mantidas fora da entrada da tabela, com
somente um apontador para cada uma dessas informações armazenado no registro.
As informações são introduzidas na tabela de símbolos em vários momentos. As
palavras-chave são introduzidas inicialmente na tabela. O analisador léxico deve
procurar sequências de letras e dígitos na tabela de símbolos para determinar se uma
palavra-chave reservada ou um nome foi coletado. Com essa abordagem, as palavras-
chave precisam estar na tabela de símbolos antes da análise léxica começar.
Alternativamente, se o analisador léxico intercepta as palavras-chaves reservadas, as
mesmas não precisam figurar na tabela de símbolos.

Você também pode gostar