Escolar Documentos
Profissional Documentos
Cultura Documentos
2. Analisador léxico
Um analisador léxico, ou scanner, é um programa que implementa um autômato finito,
reconhecendo (ou não) strings como símbolos válidos de uma linguagem.
A implementação de um analisador léxico requer uma descrição do autômato que
reconhece as sentenças da gramática ou expressão regular de interesse. Com essa
descrição, é possível oferecer os seguintes procedimentos auxiliares para o analisador
léxico:
3. Analisador sintático
Um analisador sintático para uma gramática $ G$ é um programa que aceita como
entrada uma sentença (uma lista de símbolos $ \alpha$) e constrói para a sentença sua
árvore gramatical (ou equivalentemente uma seqüência de derivação) ou, caso a
sentença não pertença à linguagem descrita por $ G$, uma indicação de erro.
4. Analisador Semântico
return a%b;
A tentativa de compilar esse código irá gerar um erro detectado pelo analisador
semântico, mais especificamente pelas regras de verificação de tipos, indicando que o
operador módulo % não pode ter um operador real. No compilador gcc, essa mensagem
é
In function `f1':
a = x - '0';
a constante do tipo caráter '0' é automaticamente convertida para inteiro para compor
corretamente a expressão aritmética na qual ela toma parte; todo char em uma expressão
é convertido pelo compilador para um int. Esse procedimento de conversão de tipo é
denominado coerção (cast). Em C, a seguinte sequência de regras determina a realização
automática de coerção em expressões aritméticas:
1. char e short são convertidos para int, float para double;
2. se um dos operandos é double, o outro é convertido para double e o resultado é
double;
3. se um dos operandos é long, o outro é convertido para long e o resultado é long;
4. se um dos operandos é unsigned, o outro é convertido para unsigned e o
resultado é unsigned;
5. senão, todos os operandos são int e o resultado é int.
int a;
int *p;
a expressão
a = p;
Porém, se o programador indicar que sabe que está fazendo uma conversão ``perigosa''
através do operador de molde,
a = (int) p;
c << x;
Outro exemplo de erro detectado pela análise semântica, neste caso pela verificação de
fluxo de controle, é ilustrado pelo código
if (j == k)
break;
else
continue;
In function `f2':
ou seja, ele reconhece que o comando break só pode ser usado para quebrar a seqüência
de um comando de iteração (within loop) ou para indicar o fim de um case (within
switch). Da mesma forma, um comando continue só pode ser usado em um comando de
iteração.
void f3(int k) {
struct {
int a;
float a;
} x;
float x;
switch (k) {
In function `f3':
A primeira mensagem detecta que dois membros de uma mesma definição de estrutura
recebem o mesmo nome, a, o que não é permitido. Na segunda mensagem a indicação
refere-se às duas variáveis de mesmo nome, x. A terceira mensagem indica que dois
cases em uma expressão switch receberam o mesmo rótulo, o que também não é
permitido. Observe que, mesmo embora a forma de expressar o valor nos cases tenha
sido diferente, o compilador verificou que 0x31 e '1' referiam-se ao mesmo valor e
acusou a situação de erro.
5. Compilador Completo
A etapa final na geração de código pelo compilador é a fase de otimização. Deve-se
construir ou atualizar a tabela de símbolos, fazer a checagem de tipos (a partir da árvore
sintática gerada) e verificar as chamadas de métodos.
Como o código gerado através da tradução orientada a sintaxe contempla expressões
independentes, diversas situações contendo sequências de código ineficiente podem
ocorrer. O objeto da etapa de otimização de código é aplicar um conjunto de heurísticas
para detectar tais sequências e substituí-las por outras que removam as situações de
ineficiência.
As técnicas de otimização que são usadas em compiladores devem, além de manter o
significado do programa original, ser capazes de capturar a maior parte das
possibilidades de melhoria do código dentro de limites razoáveis de esforço gasto para
tal fim. Em geral, os compiladores usualmente permitem especificar qual o grau de
esforço desejado no processo de otimização. Por exemplo, em gcc há opções na forma -
que dão essa indicação, desde (nenhuma otimização) até (máxima otimização,
aumentando o tempo de compilação), incluindo também uma opção, indicando que o
objetivo é reduzir a ocupação de espaço em memória.
Algumas heurísticas de otimização são sempre aplicadas pelos compiladores. Por
exemplo, se a concatenação de código gerado por duas expressões no programa original
gerou uma situação de desvio incondicional para a linha seguinte, como em
<a>
goto _L1
_L1: <b>
esse código pode ser seguramente reduzido com a aplicação da técnica de eliminação de
desvios desnecessários, resultando em
<a>
_L1: <b>
Particularmente no último caso, se x for uma variável inteira a divisão por dois pode ser
substituída por um deslocamento da representação binária à direita de um bit.
Genericamente, a divisão inteira por 2^n equivale ao deslocamento à direita de n bits
na representação binária do dividendo. Da mesma forma, a multiplicação inteira por
potências de dois pode ser substituída por deslocamento de bits à esquerda.
A utilização de propriedades algébricas permite também o reuso de sub-expressões já
computadas. Por exemplo, a tradução direta das expressões
a = b + c;
e = c + d + b;
geraria o seguinte código intermediário:
a := b + c
_t1 := c + d
e := _t1 + b
No entanto, o uso da comutatividade e associatividade da adição permite que o código
gerado seja reduzido usando a eliminação de expressões comuns, resultando em
a := b + c
e := a + d
Diversas oportunidades de otimização estão associadas à análise de comandos
iterativos. Uma estratégia é a movimentação de código, aplicado quando um cálculo
realizado dentro do laço na verdade envolve valores invariantes na iteração. Por
exemplo, o comando
Referencias
http://www.cambridge.org/resources/052182060X/MCIIJ2e/grammar.htm
http://www.cs.tufts.edu/~sguyer/classes/comp181-2006/minijava.html
https://files.cercomp.ufg.br/weby/up/498/o/NelioJunior2011.pdf
https://geovanegriesang.files.wordpress.com/2015/04/compiladores_04.pdf