Você está na página 1de 19

Compiladores

_______CAPÍTULO 1___ _______________________ _______ _

INTRODUÇÃO _

COMPILADOR: é um programa que recebe como entrada um programa em


uma linguagem de programação, e o traduz para um programa
equivalente em outra linguagem.

Caso o programa objeto for um programa em uma linguagem de máquina


executavam poderá ser chamado pelo usuário para processar entradas
e produzir saída.

INTERPRETADOR: executa diretamente as operações


especificadas no programa fonte sobre as entradas
fornecidas pelo usuário, conforme imagem ao lado.

O COMPILADOR É MAIS RÁPIDO QUE O INTERPRETADOR, POREM O INTERPRETADOR


FREQUENTEMENTE OFERECE UM MELHOR DIAGNOSTICO DE ERRO DO QUE UM COMPILADOR, POIS
EXECUTA O PROGRAMA FONTE INSTRUÇÃO POR INSTRUÇÃO.

ESTRUTURA DE UM COMPILADOR _

Existe duas partes no mapeamento entre a compilação de um programa fonte para um


programa objeto semanticamente equivalente, que são: Analise e Síntese.

ANÁLISE: Subdivide o programa fonte em partes constituintes e impõe uma estrutura


gramatica sobre elas, e logo depois usa essa estrutura para criar uma representação
intermediária do programa fonte. Caso seja detectado pela Análise, que o programa
está mal formatado semanticamente ou incorreto, ele precisa fornecer mensagem
esclarecedora para o usuário tomar uma ação corretiva. Logo após, a Análise também
coleta informações sobre o programa fonte e armazena em uma estrutura de dados
chamada TABELA DE SIMBOLOS, que é passada para SÍNTESE.

SÍNTESE: A parte de síntese constrói o programa objeto desejado a partir da


representação intermediaria e das informações na tabela de símbolos.
A PARTE DE ANÁLISE NORMALENTE É CHAMADO DE FRONT-END DO COMPILADOR, A PARTE DE
SÍNTESE É O BACK-END.

O processo de compilação é composto por uma sequência de fases, sendo a tabela de


símbolos, responsáveis pelo armazenamento das informações sobre todo o programa
fonte, é utilizada por todas aas fases do compilador.

1
Compiladores
ANÁLISE LÉXICA _

A primeira fase de um compilador é chamada


de análise léxica (scannig), onde ele lê
o fluxo de caracteres que compõem o
programa fonte e os agrupa em sequencias
significativas, chamada LEXEMAS. Para
cada lexema, o analisador léxico produz
como saída um token no formato: <nome-
token, valor-atribuído>

Feito isto, ele envia este token para a


fase subsequente, chamada de ANÁLISE
SINTÁTICA, em que o primeiro componente
“nome-token” é um símbolo abstrato que é
usado durante a análise sintática. O
segundo componente “valor-atribuído”,
aponta para uma entrada na tabela de
símbolos referente a esse token. A
informação da entrada da tabela de
símbolos, é necessária para analise
semântica e para a geração de código.

PARA DEFINIR UMA LEXAME OU REGRAS DESSE


LEXAME, É USADO AS EXPRESSOES REGULARES.

LEXEME: É a sequência que monta um


identificador, ou alguma expressão regular codificada.

ANÁLISE SINTÁTICA _

A segunda fase do compilador é a analise sintática. O


analisador sintático utiliza os três primeiros componentes
dos tokens produzidos pelo analisador léxico para criar
uma representação intermediaria tipo arvore, que mostra a
estrutura gramatical da sequência de tokens. Uma arvore
sintaxe por exemplo, defini que em cada nó interior,
representa uma operação e os filhos do nó representam os
argumentos da operação. Uma árvore de sintaxe para o fluxo
de tokens aparece como saída do analisador sintático.

O Analisador Sintático está mais interessado em receber um


identificador, ou seja, uma variável seguida de um tipo de
identificador.

ANÁLISE SEMÂNTICO _

O analisador semântico utiliza a árvore de sintaxe e as informações na tabela de


símbolos para verificar a consistência semântica do programa fonte com a definição
da linguagem. Também reúne informações sobre os tipos e as salvas na árvore de
sintaxe ou na tabela de símbolos.

Uma parte importante é a verificação de tipo, em que o compilador verifica se cada


operador possui operandos compatíveis, por exemplo, se caso se verificado um
arranjo com um índice que não seja um inteiro, e sim um ponto flutuante, pode ser
convertido pelas chamadas de coerções que são algumas especificações da linguagem

2
Compiladores
para permitir tais chamadas. Logo, essa alteração de entidade de um tipo de dados
em outro.

AMBIGUIDADE _

NA LINGUAGEM ___________________________________________________________________

Caso a linguagem seja ambígua, logo toda gramatica será ambígua.

NA GRAMATICA____________________________________________________________________

Para retirar a ambiguidade temos que saber o que a linguagem está gerando, depois
disso vamos ter que reescrever a gramatica mantendo a mesma linguagem

RECURSIVIDADE _

RECURSIVIDADE DIRETA

A recursividade direta é quando temos na variável uma recursividade chamando ela


mesma, exemplo: V: A  Aa | b

Dependendo do parsing que vamos realizar, ele não aceita um determinado formato
das expressões regulares, com recursividade a esquerda. Os parsing são realizados
por chamadas recursivas, mas não é a chamada recursiva que gera ambiguidade.

REGRA:

Temos que trocar Variável que tem


recursividade a esquerda, como ‘A’. Desta
forma, temos que parar de começar com ‘A’,
caso contrário não paramos nunca, loop.

Isto porque o analisador Léxico, não


reconhece o A, somente o que o acompanha
sendo o Alfa.

Logo, vamos aplicar a regra para retirar a


recursividade a esquerda.

EXEMPLO:........................................................................

Aplicando a regra na imagem ao lado, identificamos que em existe a recursividade


a esquerda duas vezes, possibilidade de entrar em loop.

Com isto, a imagem ao lado representa a solução pela regra inserindo duas regras,
onde essa recursividade vai ter um ponto de parada.

3
Compiladores
G: Gramatica

V: Variável

3: Alfabeto

P: Produções que podem ser


gerados, Troca A por bT, ou
cT entre outros.

ESSA OPERAÇÃO PRESERVA A


LINGUAGEM DA GRAMATICA, OU SEJA,
A LINGUAGEM NÃO MUDA MAS A
GRAMATICA SIM!

RECURSIVIDADE INDIRETA

Na recursividade indireta, temos uma variável que chama outra variável, que por
sua vez chama a variável antecessor, ou seja, indiretamente.

Nesta imagem ao lado, temos derivado o ‘S’ que chama


‘A’, e logo na variável A, vemos que ele não chamou
ele mesmo, mas quando derivamos vemos que sim, o ‘S’
chama ‘A’ e o ‘A’ chama ‘S’, logo temos um tipo de
recursividade indireta.

Neste caso, como é que transformamos essa regra da


gramatica em uma regra que não é recursiva a esquerda
e não gere outra String? Aplicando a regra.

ENTENDENDO A REGRA NOVAMENTE .....

Temos que trocar Variável que tem


recursividade a esquerda, como ‘A’. Desta
forma, temos que parar de começar com ‘A’,
caso contrário não paramos nunca, loop.

Isto porque o analisador Léxico, não


reconhece o A, somente o que o acompanha
sendo o Alfa.

Logo, vamos aplicar a regra para retirar a


recursividade a esquerda.

Agora, essa é a nova regra.

TEMOS QUE TER CERTEZA DE QUE A NOVA LINGUAGEM TEM QUE


GERAR A MESMA COISA QUE A OUTRA LINGUAGEM, POIS CASO
ALGUMA GERE ALGO DIFERENTE, ESTARÁ ERRADO! Pois estamos
garantidos que as gramaticas são diferentes, mas que
geram a mesma linguagem.

O ALFABETO TEM QUE SER O MESMO EM AMBAS AS LINGUAGENS. V: vai ser diferente,

4
Compiladores
E: expressão

É importante observar na imagem acima a ordem da precedência que no caso da


divisão, temos que fazer a subtração antes e da mesma forma na multiplicação, que
foi feito a soma antes, ou seja, acima destes. A Gramatica segue a ordem da
esquerda para direita, tipo o ‘+’ pode ter a precedência de ‘-’.

LEMBRANDO QUE A GRAMATICA NO CASO DO TRABALHO, É FEITO POR NOS ALUNOS.

É possível verificar que no exemplo acima, a recursividade está na esquerda segundo


a arvore acima, isso é necessário para dar precedência para multiplicação.

Com isso, agora temos que implementar a Gramatica-2 para este exemplo afim de
retirar a recursividade a esquerda.

FATORAR _

Fatorar a Esquerda é colocar os


elementos em comuns para não repeti-
los várias vezes.

Recursividade a Esquerda é retirar a


recursividade se não, não é feito um
parsing top-down.

PARSING TOP-DOWN _

No PARSING DOWN, ele começa a análise da primeira até a última regra, de cima para
baixo. Com isso, ele tem o problema de desempenho, pois passa pelas tokens várias
vezes. Exemplo, ele entra dentro de uma regra, caso não dê certo, ele volta para
outra expressão, logo ele tem que ler o arquivo de entrada para cada regra.

Com isso, nestas regras acima, para evitar o problema no parsing top-down, temos
que trocar a Chamada do B, por A, logo implementando a recursividade. Só que,
nisto geramos ambiguidade, no PARSING TOP DOWN.
5
Compiladores
PODEMOS TER A RECURSIVIDADE A DIREITA. MAS NÃO PODEMOS TER RECURSIVIDADE A
ESQUERDA.

No Parsing Top Down é aquilo que faz uma derivação partindo do ciclo inicial até
tentar chegar no ciclo final, se chegar está sintaticamente correto se não
sintaticamente incorreto. Logo ele está construindo uma arvore de parsing a partir
da gramatica que você deu, logo ele vai em pré-ordem.

PRÉ-ordem: RAIZ  ESQUERDA  DIREITA.


EM-ordem: ESQUERDA  RAIZ  DIREITA.
PÓS-ordem: ESQUERDA  DIREITA  RAIZ.

No TOP-DOWN a não pode ter que recursividade a esquerda, pois entre em um loop. A
gramatica começa como simplesmente legível e com recursividade a esquerda, ou
seja, é fácil implementar de forma legível e com as precedências necessários,
depois temos que retirar a recursividade a esquerda, trazendo para a direita.

PRECEDENCIA: A ideia é quem ta mais no fundo ter a precedência, pois ele funciona
em pré-ordem. Logo, NESTE EXEMPLO QUE TEM MAIOR PRECEDENCIA É QUE TA MAIS NO
FUNDO, COMO A MULTIPLICAÇÃO TEM MAIOR PRECEDENCIA QUE A SOMA, E NO ‘F’ O PARENTESE
TEM MAIOR PRECEDENCIA QUE A MULTIPLICAÇÃO.

1º - Para implementar essa gramatica em TOP-DOWN,


temos que analisar:

2º RECURSIVIDADE: Se tiver recursividade, temos que


retirar essa recursividade.

Logo retirando a recursividade através da formula, verificamos que a recursividade


fica à direita, logo atendendo ao PARSING TOP-DOWN.

O PARSING TOP-DOWN, PODE TER OU NÃO UM BACKTRANKING, POIS EM CASO MAIS SIMPLES
ELE TEM BACKTRACKING. EM OUTROS NÃO, MAS FICA INVIAVEL SUA IMPLEMENTAÇÃO.

AULA – 23/10/2017

Para implementação do parsing top-down, precisamos acima de tudo de eliminar a


recursividade a esquerda, para que ele não estoure a pilha de memória.

Em segundo, para eliminar o backtracking,


podemos fatorar se necessário, afim de dele
não ficar testando várias vezes – indo e
voltando.

DESCEDENTE – Ele faz a análise da primeira


variável até expandir para chegar no
terminal.

6
Compiladores
No algoritmo, cada
variável vira uma
rotina, logo na imagem
ao lado, temos a
gramatica com duas
variáveis, logo duas
rotinas.
Cada regra da variável
simula a variável, sendo
que ele chama o lexan
para verificar a token,
se for verdadeiro ele
retorna 1, se falso,
retorna 0.

Na variável A, temos
duas opções para testar
a variável – regras,
logo caso a primeira não
de certo, ele tem que
voltar e testar na B, ou
seja, segurar o lexeme
para não avançar para o próximo.

Logo, no programa eu tenho a função da primeira variável A(), quando chama-la se


retornar verdadeiro-1, OK, se falso eu texto a próxima variável.

Logo na variável A, percebemos que ele queria um identificador (a) seguido de


constante (b).

BACKTRACKING: Simulando que estamos lendo o lexeme = IDADE 12345, chamando a


primeira função o i=0 percorre até 1=5 e verifica que encontrou um identificador,
logo a função testa se é verdadeiro pois os vai ser se o retorno for == a, sendo
‘a’ programado que é um identificador, então ele retorna verdadeiro e continua a
percorrer. Quando o i=12 ele encontra uma constante, e novamente a primeira função
que é A() testa se é um identificador, como não é ele volta o i=5 e passa para a
próxima função testar se é uma constante. Logo, isto é, backtracking.

PARSING PREDICTIVE _

Neste parsing é ser prever o futuro, preditivo que vai determinar algo, tipo estou
prevendo que vou passar em todas as matérias com nota alta. LOGO NÃO CONTEM
BACKTRACKING.

Logo é necessário ELEMINARA RECURSIVIDADE A ESQUERDA E FATORAR.

1º - Para implementar essa gramatica em TOP-DOWN, temos que analisar:

2º FATORAR: Se tiver elementos em comum, fatorar primeiro

3º RECURSIVIDADE: Se tiver recursividade, temos que retirar essa recursividade.

Esta implementação roda de 20 a 25% mais rápida que a não simplificada, logo,
aplicando os conceitos acima.

O problema de um parsing não predicitive, é que temos que testar vários caminhos.

PREDICITIVE PARSING NÃO RECURSIVO

Neste parsing, ele utiliza uma tabela de parsing para determinar a produção a ser
usada.

7
Compiladores
VANTAGEM DESTE PARSING, É QUE SE MUDAR A GRAMATICA, BASTA GERAR UMA NOVA TABELA E
NÃO TER QUE GERAR UM PARSING NOVO.

Se não fatorar ou retirar a recursividade a esquerda deste parsing, a tabela terá


inúmeras entradas. Logo, caso esteja múltiplas entradas nessa tabela, está errado!
A gramatica não serve para esse parsing.

TOP DOWN vs PREDICITIVE


No parsing top-down seja ele qual for, tem que retirar a recursividade, ele pode
ter um backtraking. No predicitive, temos que retirar a recursividade e ainda
fatorar, com isso, ele não realiza o backtraking.

Componentes:

1. Buffer de entrada: guarda o String de tokens (o fim do String é indicado


por $), logo o $ no analisador sintático das aulas práticas, vão terminar
com este caractere.
2. Pilha: armazena uma sequência de símbolos da gramática com $ marcando o
fundo da pilha.
3. Tabela de parsing
3.1 Arranjo bidimensional M[ A, a ]
3.2 Armazena as produções a serem usadas
3.3 Guia as ações do parser

TABELA

Para gerar a tabela, utiliza-se duas funções sendo o FOLLOW e o FIRST. O First me
dá as primeiras tokens que uma variável pode derivar, ou seja, ele me dá a primeira
variável que a regra deriva.

Follow é seguinte ideia é que ele segue alguém, e no código, um elemento só pode
se anular se caso ele está sendo seguido por alguém. Logo, o Follow olha para a
variável e vem que a esta seguindo, por exemplo na imagem abaixo, quem está
seguindo a variável B são todos os símbolos gerados por C.(isto por causa da regra
em A,‘BC’ = B seguido de C). O Follow diz que só pode se anular quando ele tiver
certeza do que vem depois de você é gerado pelo próximo símbolo, caso contrário
não pode se anular.

A ideia do código é que, se caso uma entrada não ser resolvida na primeira regra
da Variável A ele passa para a segunda regra, caso não resolva ele se se anula
para outro poder resolver, no caso, se B seguido de C: se o B se anular, ele passa
a bola para o C gerar o símbolo.

Analisando o exemplo
abaixo, temos que o
First da Variável C
gera ‘d’ e lambida,
consequentemente as
outras também geram
seus próprios
símbolos. Detalhe que
na variável A está
seguido de outras variáveis, e essas variáveis também geram símbolos, logo tudo
que essas variáveis geram, o A também gera. Lembrando que o A por gerar lambida,
ele não gera direto a lambida, pois a variável C tem que se anular.

Exemplo: Se o B se anular, temos que olhar o First de A, pois as variáveis que se


anulam é porque possuem lambida.

8
Compiladores
GERANDO FIRST E FOLLOW

Começa-se a analise com o E que esta seguido de $.

O First de E é o que o T gera com primeiro símbolo, e o T gera F como primeiro


símbolo, então, o F gera como primeiros símbolos que é o ‘(‘ e o ‘id‘, pois são
os primeiros símbolos da regra de T.

REGRA: Tudo que segue a primeira variável, vai seguir também a sua derivada,
exemplo: tudo que segue E, vai seguir E’; Tudo que segue T, vai seguir T’...

O PROBLEMAD ESTE PARSIN É QUE ELE PARA


NO PRIMEIRO ERRO, NÃO CONTINUA A
TENTATIVA.

A QUALIDADE DESTE PARSING, É QUE CASO


A GRAMATICA MUDAR, É SOMENTE MUDAR
TAMBEM A TABELA E NÃO NECESSARIO MUDAR
O ALGORITIMO.

ESTE PARSIG É DECEDENTE DE CIMA PARA


BAIXO, QUE NÃO FAZ UM BACKTRACKING.

CASO AO GERAR A TABELA E TIVER


MÚLTIPLAS ENTRADAS, NÃO É RECOMENDADO
USAR ESTE PARSING. Sendo assim, ou
mude a gramatica ou o parsing.

ESSA TABELA GERADA PELO PREDICTIVE É


LL(1), E CASO UTILIZE O PARSING
ACEDENTE QUE GERA UMA TABELA SLR,
sendo o último uma tabela muito mais
difícil de se gerar do que o primeiro. Entretendo o ultimo, analisa uma gama de
gramatica que o outro não consegue analisar.

LOGO NÃO É GARANTIA QUE AO FATORAR E RETIRAR A RECURSIVIDADE A ESQUERDA, SERÁ


IMPLEMENTADO COM SUCESSO O PREDECTIVE, POIS PODEM TER MULTIPLAS ENTRADAS AO GERAR
A TABELA.

9
Compiladores
GERANDO A TABELA

Os terminais são ‘a’, ‘b’ e ‘$’, estes são implementados na tabela com os terminais
na primeira linha. E as variáveis.

Na primeira linha, da variável A e coluna ‘a’, identificamos que se tivermos com


o A na mão e o ‘a’ na entrada, trocamos ‘A’ por ‘aA’ (A->aA). Da mesma forma que
a próxima regra de A, quando tiver ‘b’ na entrada, vamos trocar por A->B.
É POSSIVEL QUE SE TIVERMOS O A NÁ MÃO E CHEGAR O ‘$’, QUE DIZER ERRO POIS SERIA
ESPAÇO EM BRANCO. LOGO OS ESPAÇOS EM BRANCOS NA TABELA SÃO ESTADOS DE ERROS.

No B, dado que tenha uma entrada ‘b’, troamos B->b.

O algoritmo para isso, precisa de uma pilha e começa com A$, pois o A é o símbolo
inicial.

NESTE ALGORITIMO NÃO TEM CHAMADA DE FUNÇÃO E SIM UM LOOP, QUE FICA NA SEGUINTE
CONDIÇÃO: ENQUANTO NÃO TEM ERRO E NÃO ESTA NO TOPO DA PILHA O $, EU CONTINUO.

Seguindo a imagem ao lado, temos um caminho


percorrido pelo algoritmo sendo que a primeira
coluna como o início do algoritmo, logo ele verifica
se casa e olha na tabela, se não erro sintático.

10
Compiladores
EXEMPLO – 2 ____________________________________________________________________

Neste exemplo temos a seguinte gramatica na imagem ao lado no qual foi retirado a
recursividade a esquerda, gerando a gramatica G2.

Caso seja necessário fatorar, teria que fatorar, mas neste caso vemos que não é
necessário.

DICA:

1º - FIRST: Geramos ele começando de cima para


baixo, da variável F -> E.

2º - FOLLOW: Geramos de cima para baixo, começando


de E -> F

PARSING BUTTON-UP _

 PARSING DESCENDENTE – TOPDOWN, nós temos um símbolo inicial que começa a fazer
derivações até chegar em uma String final. Qualquer que seja o top-down, temos
que retirar a recursividade a esquerda.

 PARSING PREDICTIVE, temos que gerar a tabela, calcular First e Follow, temos
que fatorar a esquerda. Este fato de fatorar e tirar recursividade, não garante
que seja capaz de gerar este parsing pois caso a gramatica seja ambígua,
teríamos múltiplas entradas na tabela de parsing.

 PARSING ASCENDENTE - BUTTON-UP, ele não precisa de fatorar nem tirar a


recursividade, assim ele pode reconhecer uma gramatica maior que os outros,
mas sendo difícil de implementação e pouco complexo.

11
Compiladores

Nesta imagem, percebemos a diferença entra um parsing


descendente que desce até expandir toda regra até uma
String. Com isso, o parsing ascendente ele reduz até
chegar em um símbolo inicial.

CONFLITOS

REDUCE-REDUCE

Na imagem abaixo, temos um exemplo de um conflito, em que


podemos pegar o ‘a’ e reduzir até chegar a variável A.
Soque que podemos pegar o ‘a’ também e reduzir até chegar
na variável B. Com isto, temos um ponto de conflito
conhecido como REDUCE-REDUCE.

Este tipo de conflito não existe no


parsing top-down. Este parsing, tem
que determinar qual o elemento que
vai reduzir, no qual chamamos de
HANDLE. Este Handle, é o elemento
que temos na mão que ao reduzir vai
levar a um passo que se chega ao
símbolo inicial.

Lembramos que quanto se tem


recursividade a esquerda ou direita, quer dizer que a gramatica tende a ser
ambígua, mas as vezes não é.

SHIFT-REDUCE

Neste conflito shift-reduce, ocorre quando uma opção de poder reduzir e também de
empilhar na máquina para poder ser encontrado. Um exemplo, é ter um elemento que
já foi encontrado e aí a máquina diz que podemos reduzir, mas também tem um ainda
está pendente de leitura, não encontramos, logo nos deparamos com um conflito de
adicionar e retirar da pilha. Quando temos este problema, nós decidimos ir para o
caminho de empilhar, ou seja, jogar fora o reduce e fazer o shift.

IMPLEMENTAÇÃO DO PARSING BUTTON-UP (DESCEDENTE)

1. Temos que aumentar a gramatica, adicionando um símbolo inicial no qual é o de


parada.

2. Temos que calcular o First e Follow. (TODA REDUÇÃO USA FOLLOW NA TABELA)

3. Gerar uma coleção de itens LR, que é da esquerda para a direita, se ela fosse
LR1 teríamos que ler pelo menos 1 elemento na entrada para saber o que fazer,
e assim por diante se fosse LR2, LR3.

4. Montar tabela AFD, essa tabela vai representar um autômato finito determinista,
baseado nas regras da gramatica. Ele vai mostra como a partir do símbolo
inicial nós vamos ir trocando até chegar no String final.

MONTAR TABELA

1. Aumentar a Gramatica
2. Gerar Coleção de Itens
3. Preencher a tabela com a coleção de itens, baseado em First e Follow.

12
Compiladores
Gerando a coleção de itens, através da imagem abaixo, temos que pegamos a gramatica
e geramos uma coleção de itens que vão ser trocados.

Após gerar a coleção de itens, pode-se


perceber que cada item é um estado da
máquina sendo assim que definido por um
PONTO que diz qual o próximo símbolo que
posso ser trocado. Este ponto mostra que
já processamos e o que falta processar,
logo do ponto para a direita, sentido de
esquerda para a direita.

SABEMOS QUANDO ESTAR SINTATICAMENTE


CORRETO QUANDO REDUZIMOS UMA E  S.

I1: Neste estado quando temos o ponto depois do E,


q quer dizer que já podemos trocar o E para o S.

I2: Neste estado, podemos trocar o T por E.

Sempre que chegar no PONTO e o ponto tiver enxergando o T por exemplo, temos que
expandir o T em outro estado, pegar um estado IX e
expandir todas as possibilidades que o T vai produzir.

 No final de alguns estados,


temos que ele aponta para outro
estado, por exemplo em I6 que ao
ler F, aponta para o estado I8.

Neste passo, temos que gerar o


First e Follow para montagem da
tabela.

13
Compiladores
Para montagem da tabela
temos que colocar Os
ESTADOS, sendo de 0  8.

Os terminais são:
+, *, K e $

Também, temos as
variáveis em transição:

E, T, F

ACTION:
Representa as transições
sobre os elementos do
alfabeto.

GO-TO:
Representa as transições
das variáveis. Se
estiver no estado 0 e
estiver lendo um E, vai
para o estado 1.

14
Compiladores

_______REVISÃO____________________________________________________________________
SIMBOLOGIA _

L: linguagem que contém ou gera uma


gramatica

G: Gramatica

V: Variável

3: Alfabeto

P: Produções que podem ser gerados, Troca A


por bT, ou cT entre outros.

EXPRESSÃO REGULAR _

Expressão regular é formada a partir de um alfabeto, utilizando somente três


operações.

 = união; * = Fecho;  = Concatenação; + = Pelo menos;

*FECHO: O fecho permite entrar em um conjunto e pegar quantos unidades do alfabeto


quiser, sendo assim, a partir de um conjunto FINITO gera um conjunto a
princípio IFINITO. Se o conjunto for nada, ele vai gerar uma String de
Tamanho ZERO, e não “gerar nada”.

+ PELO MENOS: Quando é inserido o (+), quer dizer que temos que pelo menos pegar
1 caractere dentro do (+).

EX: ∈ {𝒂, 𝒃}∗ - Se não entrar no conjunto, é gerado lambida, e ao entrar você pode
pegar quantos do alfabeto quiser.

O identificador é basicamente interpretado por expressões regulares, bem como,


defini regras de como começa, compõem, e termina o identificador. É seguido por
uma regra pré-determinada.

EX: 𝑰𝑫𝑬𝑵𝑻𝑰𝑭𝑰𝑪𝑨𝑫𝑶𝑹 → 𝑳𝒆𝒕𝒓𝒂 {𝒍𝒆𝒕𝒓𝒂 | 𝑫𝒊𝒈𝒊𝒕𝒐} - Neste exemplo, estou dizendo que
meu identificador é composto por uma letra ou digito, no mínimo uma letra, logo
meu identificador tem somente 2 caracteres.

EX: 𝑰𝑫𝑬𝑵𝑻𝑰𝑭𝑰𝑪𝑨𝑫𝑶𝑹 → 𝑳𝒆𝒕𝒓𝒂 {𝒍𝒆𝒕𝒓𝒂 | 𝑫𝒊𝒈𝒊𝒕𝒐}∗ - Agora, estou dizendo que meu
identificador pode ter vários caracteres composto por letra ou digito.

EX: 𝑎∗ 𝑎∗ = 𝑎∗ ; 𝑎+ 𝑎+ = 𝑎𝑎+
PRESEDENCIA DE OPERADORES

Em uma expressão regular para regra de quem tem precedência, vai depender da
expressão. Primeiro normalmente é feito o (1)FECHO, (2)CONCATENAÇÃO, (3)UNIÃO,
isto em um exemplo apenas, vai depender de expressão para expressão.
+ + +
EX: 𝐶𝑜𝑛𝑠𝑡𝑎𝑛𝑡𝑒 → 𝐷 | 𝐷 . 𝐷 - Caso venha como entrada “1.42”, ele deverá
primeiro identificar como DIGITO(+).DIGITO(+), pois caso contrário, ele vai enviar
para a próxima fase como um caractere que não é identificado. Com isto, vemos a
ordem de precedência que foi a concatenação antes da união.

15
Compiladores
AUTÔMATOS _

Toda expressão regular é reconhecida pelo autômato, logo toda expressão regular
corresponde ao autômato finito determinista ‘m’ cuja linguagem de ‘m’ é essa
expressão. Toda vez que escrevemos essa expressão, temos que ter uma máquina que
reconheça essa expressão.

Iniciamos para reconhecer essa entrada, logo


quando for um digito, vai para o estado 2,
enquanto estiver lendo um digito, ficamos no
estado 2. Quando vier alguma coisa diferente
de um digito, vai para estado 3 (Estado de
Aceitação = Final), que é no final. Logo, neste
exemplo lemos 4 caracteres, 3 dígitos e 1 sinal
que é outra coisa (desconhecida deste
autômato). Sendo assim, ele volta 1 caractere e completa os dígitos 123 conforme
a regra da expressão regular pré-determinada para ser assim, daí ele envia os
dígitos mais o Sinal (e tudo que não reconhecer na regra) para a próxima fase.

Para implementação deste autômato, normalmente é utilizado estruturas como: Switch


Case e Do While.

AFND: O Autômato Finito Não-Determinístico, é quanto o próximo estado é


indeterminado, exemplo quando se tem vários caminhos onde se testa uma entrada
até achar o estado de aceitação, logo ele testa um caminho se não der certo, testa
outro e por aí vai.

AFN: O Autômato Finito Determinístico, é quando seu próximo estado é possível de


ser determinado, logo sendo um caminho só para executar uma entrada.

!! GRAFO LEXO É UM AUTOMATO !!

GRAMATICA _

A gramatica é fundamental para ANALISE SINTÁTICA. A gramatica é um mecanismo


formal, para definir regras que vão impor restrições não mais para denotar um
conjunto de strings, mas para derivação destas, ou seja, para chegar em um conjunto
de strings

Uma expressão denota uma linguagem (a*b, representa uma linguagem com =
aa,ab,abb...); Já a gramatica, ela deriva gera a partir de uma regra, algo que
possamos rever.

A analisa sintática, vai dizer se está certo a combinação do elemento ou não,


gerado pelo analisador léxico. Com isto, a gramatica é quem vai definir com base
nas suas regras.

Seguindo a ideia da gramatica abaixo, temos um alfabeto (E: identificador, =,


constante) no qual através do da entrada abaixo, a derivação fica em 1)Declaração
– gera 2)Identificador, seguido de atribuição, seguido de uma constante. E isso
vai sendo derivada conforme a gramatica.

16
Compiladores

Seguindo o exemplo da
gramatica abaixo:

ALFABEO DO EXEMPLO É:
Identificador, +, * e k-
constante.

Definimos uma gramatica com algumas 2 expressões, que são elas: ATRIBUIÇÃO e
EXPRESSÃO. Dentro de cada expressão temos regras, que neste exemplo são 5, em que
são:

1° REGRA – “ATRIBUIÇÃO produz ID recebe expressão atribuição”;


2° REGRA – “ATRIBUIÇÃO produz lambida”;
3° REGRA – “EXPRESSÃO produz expressão + expressão”;
4° REGRA – “EXPRESSÃO produz expressão*expressão”;
5° REGRA – “EXPRESSÃO produz k-constante”;

Logo, a tradução fica:

1) ATRIBUIÇÃO > é > IDENTIFICADOR recebe EXPRESSÃO ATRIBUIÇÃO

1.1) Com isso, vamos cortando conforme é traduzido a entrada.


“a=45+10*3” -> ‘a’ é um identificador; ‘=’ é uma atribuição, ‘45’
?
1.2) Agora temos que continuar derivando para ler o resto da entrada:
‘45’ é uma expressão, derivando: expressão é expressão+expressão,
derivando: expressão é k-constante, expressão é
expressão*expressão, derivando: expressão é k-constante.

2) Com isto, essa entrada se encaixa na regra pré-definida na gramatica.

Com isso, nessa gramatica tem regras como não posso dividir ou
fazer outras coisas.

Ainda assim, temos alguns problemas na gramatica como poder gerar


ABMIGUIDADE. Uma gramatica ambígua significa que tem mais de uma
interpretação no que está sendo escrito. Nisto, se a gramatica
está gerando duas ou mais arvores na mesma string, é ambígua. Por
definição: Ambiguidade é o fato de poder interpretar uma sentença
de duas maneiras diferentes.

17
Compiladores
Neste mesmo exemplo, poderíamos ter gerado os seguintes resultados:

a = (45+10)*3 = 165; OU a = 45+(10*3) = 75;


Isto acontece, conforme a determina ordem de
leitura da arvore, e de uma delas, conforme a
imagem ao lado. Temos um crescendo para a
esquerda e a outra para direita, com isto, uma
hora você está dando precedência para SOMA,
outra hora para MULTIPLICAÇÃO.

PARA ACABAR COM A AMBIGUIDADE, TEMOS QUE


REESCREVER A GRAMATICA.

Neste outro exemplos, tambem ocorre a ambiguidade na gramatica.

_______TRABALHO PRATICO _____________________ _______ _

TEORIA COMPLEMENTAR – EXEMPLO _

TEORIA COMPLEMENTAR ATRAVÉS DE UM EXEMPLO:

A partir do nosso conjunto de regras ou seja, conjunto de analise léxicas abaixo,


vamos realizar alguns exemplos de possíveis erros na programação.

Exemplificando sobre as
constantes e os operadores, caso
seja recebido uma entrada do
tipo: 3++4 - Percebemos que é
composto por uma Constante;
Operador; Operador; Constante.

18
Compiladores
Logo percebemos que para nossa analise léxica, é preciso
conhecer o que está vindo e o já foi lido, ou seja, olhar para
a frente e também para trás, como neste caso que seria um erro
vir dois operadores seguidos.

Verificamos que desta forma que tem que ser feita a


tradução de uma entrada, como em nossas regras de
constante e identificador, o que foi diferente disto é
erro. Na imagem ao lado, temos que os dígitos são K
(constantes), e os demais são erros, também IDADE como um identificador
reconhecido, pois este foi programado lá em cima na primeira imagem deste problema.

Agora, temos que montar nosso grafo Lexo, que na verdade é um autômato
determinístico. Após isto que conseguimos gerar o programa, codificação.

RESALTAMOS QUE NESTE PROGRAMA ESTAMOS LENDO DE UM VETOR E CODIFICANDO EM UM VETOR,


MAS NO TRABALHO PRATICO, SERÁ TUDO DE UM ARQUIVO, OU SEJA, PRECISAMOS SABER
MANIPULAR ARQUIVOS TXT.

1) Devemos fazer a regra do que é


mais comum para não ficar testando
todos os casos, e ir eliminando
direto. NO AUTOMATO não preocupamos
com a ordem, MAS no GRAFO LEXO, sim!

Na primeira máquina, temos uma


sequência que montou o identificado,
logo, a sequência que monta é chamada
de lexeme de um conjunto de caracteres
reconhecido por uma expressão regular.

Quando programamos uma expressão que


neste caso é o IDENTIFICADOR, QUE NA
VERDADE É UMA TOKEN. Token é o elemento
da gramatica, o valor semântico da palavra e o lexeme é a tal palavra. Logo temos
várias sequencias que montam um identificador, e essa sequência está na regra pré-
definida lá em cima.

TOKEN É O VALOR SEMANTICO DO LEXEME

Exemplo: Ao chegar no estado de aceitação, é porque reconhecemos um identificador, nesse


caso acabamos de reconhecer um identificador “IDADE”, cadê a sequência que montou esse
identificador? A sequência que montou esse identificador, vamos chamar de lexeme, logo,
IDADE é uma sequência de caractere lexeme que montou IDADE pela token identificador.
Como token é o elemento da gramatica, valor semântico do lexeme, logo o valor semântico
de IDADE é IDENTIFICADOR. Qual o lexeme? IDADE.
No exemplo, montamos separados as maquinas em que cada uma é responsável por uma
token. Com isso, neste exemplo temos:

1- Compostos de 2 tokens;
2- Composto por 2 maquinas;
3- Uma máquina de erro;

Lembrando, que poderia ser otimizado a máquina mas por motivo de respeitar as
regradas das tokens e também para manutenção, criamos essas maquinas separas sem
uma otimização que poderia ter no final apenas um grafo lexo, uma máquina.

19

Você também pode gostar