Baixe o app para aproveitar ainda mais
Prévia do material em texto
UEPB- Universidade Estadual de Campina Grande Campus I – Campina Grande/PB Discente: Franklin da Silva Basilio Docente: Cheyenne Ribeiro 2020.1 Compiladores: Geração de Código Intermediário Novembro de 2020 Um compilador é idealizado se dividindo em front-end, back-end, onde no front se analisa o programa fonte e cria uma representação intermediária, onde os detalhes da linguagem fonte permanece aqui, enquanto no back, se concentra a geração do código objeto para a máquina alvo; desta forma uma codificação intermediária adequada poderá gerar várias saídas com os dados do front-end e back-end, onde desses dois podemos ter diversas soluções de compiladores: front x back soluções. No verificador estático, a etapa antes do gerador de código intermediário, ocorre a verificação de tipo, que garante que os operadores são aplicados a operandos compatíveis, e também inclui quaisquer verificações sintáticas que restarem após a análise sintática; Exemplo na linguagem C, verifica se o comando break está incorporado em comando while; Geração de Código Intermediário Na etapa do gerador de código intermediário, ocorre a transformação do código fonte para uma representação intermediária afim de que dependendo do destino final ela possa ser modificada para a máquina alvo. Nesta etapa não especifica detalhes de quais os registradores serão usados, quais endereços de memória serão referenciados, evitando assim deixar o código preso a uma arquitetura de uma máquina final. Com isso temos um código otimizado que auxilia na implementação de um compilador mais simples, atendendo o objetivo final, além de que esse código intermediário possa ser usado para diversas máquinas diferentes; Dependendo do destino final, a representação pode ser de baixo nível quando as tarefas dependam da máquina ou até de alto nível quando não dependerá da máquina que o código rodará, a exemplo de um programa escrito em Java, que rodará em uma máquina virtual java. Mas como desvantagem se torna um passo a mais para o compilador realizar, podendo levar um tempo extra e uso de poder computacional, ou seja, dependendo do objetivo pode ser um passo a ser pensado em ter ou deixar de ter, dependendo do destino. Analisador Sintático Verificador Estático Gerador de código intermediário Gerador de Código Código intermediário Front-end back-end Codificação de três endereços É construído a partir de dois conceitos: endereços e instruções; em termos da orientação por objetos, esses conceitos correspondem a classes, e os vários tipos de endereços e instruções correspondem a subclasses apropriadas. Como alternativa, o código de três endereços pode ser implementado usando -se registros com campos para os endereços; Neste tipo de codificação existe no máximo um operador do lado direito de uma instrução; ou seja, não haverá construção de expressão aritmética com vários operadores; Os endereços podem ser: • Um nome; • Uma constante; • Um temporário gerado pelo compilador- muito utilizado e aumenta a eficiência de um compilador otimizado; Dentre as instruções temos: • Rótulos simbólicos • Instrução de atribuição x= y op z; op aritmético binário ou lógico • Instrução x = op y; op unário • Instrução de cópia x = y • Desvio incondicional goto L; • Desvio condicionais da forma if x goto L ifFalse x goto L if x relop y goto L • Chamadas de procedimentos e retornos Param x – parâmetros Call p, n – chamada de procedimento Y = call p, n – chamada de função Return y – retorno • Instruções de indexadas de cópia: o X = y[i] – atribui a x o valor contido no endereço y[i] o X[i] = y – atribui o conteúdo do endereço x[i] a y • Atribuições de endereços e apontadores: o X = &y - x recebe o valor do endereço y; o X = *y - o *x = y - valor do objeto apontado por x com valor de y; Logo uma expressão do tipo, x + y * z deve ser particionada para se adequar ao sistema de instrução de 3 endereços, logo podemos fazer, que t1 = y*z --- t2 = x+t1 -- Neste caso em questão t1, e t2 foram gerados temporariamente pelo compilador para satisfazer o código de três endereços; a instrução básica, a avaliação de expressões aritméticas x=y operador z O nome advém dessa forma de instrução na qual ocorre a manipulação de até 3 endereços na memória: x, y, z; Declarações de variáveis: T → B { T = b.type ; w = B.width; } B→ { B.type = float ; b.width = 8; } A escolha dos operadores permitidos determinam a qualidade da forma intermediária, quanto mais próximo das instruções da máquina facilitam a implementação da forma intermediária para a máquina alvo; quanto maior for as sequencias de instruções mas trabalho terá o gerador de código e o otimizador para reestruturar as instruções e gerar o código na máquina alvo; Para essa etapa ainda temos as variantes do código de três endereços, as quadruplas e as triplas. Exemplo: Código de três endereços Quádruplas Triplas t1 = minus c t2 = b* t1 t3 = minus c t4 = b* t3 t5 = t2 + t4 a = t5 op arg1 arg2 result minus C t1 * b t1 t2 minus c t3 * b t3 t4 + t2 t4 t5 = t5 a op arg1 arg2 minus c * b (0) minus c * b (2) + (1) (3) = a (4) ➔ Como entrada para a etapa de geração de código intermediário temos a árvore sintática abstrata e a tabela de símbolos; ➔ Como saída temos um código com representação mais próxima do código-alvo, quanto mais complexo for, mais demorado será a reestruturação do código na fase seguinte; Podendo ser em maior ou menor nível de abstração; utilizando ou não informações da máquina alvo; Tradução de Expressões Nesta etapa ocorre a mudança/tradução das instruções, a exemplo de expressões com mais de um operador, que será transformada em instruções de no máximo um operador por instrução; Dado o exemplo de E → E1 +E2 geram código para computar o valor de E a partir dos de E1, E2; Se E1 for calculado e armazenado no endereço. E1.addr e E2 calculado e armazenado no endereço E2.addr, logo E1+E2 é traduzido para um temporário, t, t = E1.addr + E2.addr e em seguida E.addr recebe o valor de t; T1,t2,t3... são criados sucessivamente a medida que for sendo necessário por new temp(); Para a tradução de expressões é utilizado algumas funções e instruções típicas do gerador do código intermediário, dentre elas citamos: • A notação gen(x ’=’ y + z ), representa uma instrução de três endereços do tipo x = y + z ; onde x,y,z representam variáveis que são avaliada ao passarem por gen, que esta é uma função que monta uma instrução e a retorna; o que estiver entre apóstrofos, ‘’, será considerado literal. • A função top.get determina o endereço para o identificador, exemplo: E→ id – top.get(id.lexeme) • New Temp() é o comando para gerar nomes temporários, variáveis temporários a nível de compilador; • Code representa um conjunto de codificação em formato de três endereços; E.code B.code; • Addr representação de endereço utilizado por alguma variável; E.addr, B.addr; • Offset, obtém o próximo endereço relativo disponível; por padrão antes de utilizá-lo, é chamado e zerado; • Top.put() cria uma entrada na tabela de símbolos; exe: top.put(id.lexeme) Outro ponto é a tradução incremental ao invés de realizar uma única tradução, que já se torna resolvido por meio da codificação em três endereços que além de particionar instruções grande, ela permite que o valor de uma variável seja incrementado em outra variável temporária; Como exemplo imagine, S → id = E Onde podemos fazer { gen ( top.get (id.lexeme) ‘=’ E.addr); } Ou então, E→ E1 + E2 { E.addr = new Temp(); gen(E.addr ‘=’ E1.addr ‘+’ E2.addr); } E → - E1; { E.addr = new Temp(); gen(E.addr ‘=’ ‘minus’ E1.addr); } Campos em registrose classes: T → record ‘{‘ D ‘}’ Endereçamentos de elementos de arranjo: Os elementos de um arranjo podem ser acessados rapidamente se forem armazenados em um bloco de endereços consecutivos, onde o termo base é o endereço relativo de memória alocado para o arranjo, tipo A[0] Fluxo de Controle A tradução de comandos do tipo if-else e while está ligada a tradução das expressões booleanas, onde estas expressões são utilizadas para: • Alterar fluxo de controle; - são usadas como expressões condicionais. • Computar valores lógicos; - representando true ou false como valores, podendo ser usados em instruções de três endereços com operadores lógicos; Uma expressão após a palavra reservada IF é usada para alterar o fluxo de controle, enquanto outra, do lado direito de um comando de atribuição, é usada para denotar um valor lógico; O comando de IF, if ( x <100 || x > 200 && x!= y ) X = 0; If x < 100 goto L2 ifFalse x > 200 goto L1 ifFalse x!= y goto L1 L2: x = 0 L1: while ( i < 10 * j ){ a[ i ] = i + 2*j ; ++ i; } _L1 _t1 := 10 * j If i > = _t1 goto _L2 _t2 := 2 * j _t3 := i + _t2 _t4 := 4 * i a[_t4] := _t3 i := i+1 goto _L1 _L2: ... Se formos por exemplo computar o fatorial de um número, teremos em codificação a seguinte apresentação; Exemplo de Fatorial com IF Três endereços read x; if 0 < x then fatorial := 1; repeat fatorial := fatorial * x; x:= x-1; until x = 0; end_if read x t1 = x > 0 if_false t1 goto L1 fatorial = 1 label L2 t2 = fatorial * x fatorial = t2 t3 = x-1 x = t3 t4 = x == 0 if_false t4 goto L2 write L1 halt S → if B S1 Else S2 B.true = newLabel () B.false = newLabel () S1.next = S2.next = S.next S.code = B.code ||label ( B.true ) || S1.code ||gen (‘goto’ S.next ) ||label ( B.false ) || S2.code 1 e 2 significa a ordem dos endereços dos comandos S→ while ( B ) S1 Begin = newLabel () B.true = newlabel () B.false = S.next S1.next = begin S.code = label ( begin ) || B.code ||label(B.true) || S1.code | do i = i+1; | while (a[i] < v) | t1 = i+1 | i = t1 | t2 = i *8 | t3 = a[t2] | if t3 < v goto L Bibliografia AHO, Alfred et al. Compiladores: Princípios, Técnicas e Ferramentas. 2a Edição PEARSON – Addison Wesley, 2007. RICARTE, Ivan. Introdução à compilação. ELSEVIER, 2008
Compartilhar