Baixe o app para aproveitar ainda mais
Prévia do material em texto
DESCRIÇÃO O processo de tradução para criação de um código em linguagem de máquina, apresentando os passos da compilação, a estrutura funcional de um compilador e seus componentes. PROPÓSITO Apresentar as diferenças entre os diversos tipos de tradutores e compreender o funcionamento básico de um compilador. OBJETIVOS MÓDULO 1 Descrever os processos de tradução de um programa escrito em uma linguagem de programação para linguagem de máquina MÓDULO 2 Identificar os passos do processo de compilação e de seus produtos MÓDULO 3 Analisar a estrutura funcional do compilador e de seus componentes INTRODUÇÃO Você já parou para pensar como um programa escrito em uma linguagem de programação qualquer é convertido para um conjunto de comandos que podem ser executados por um computador? Uma CPU somente consegue manipular 0 e 1, ou seja bits, de forma que, para um programa ser executado, ele deve passar por um processo de transformação de suas instruções, escritas em alguma linguagem de alto nível, para um conjunto de instruções em linguagem de máquina, que é aquela que efetivamente o processador consegue executar. Este processo de transformações é, de uma forma geral, denominado tradução, podendo ser realizado de várias formas. Neste tema, iremos apresentar as diversas formas de tradução e nos deteremos mais especificamente na compilação. MÓDULO 1 Descrever os processos de tradução de um programa escrito em uma linguagem de programação para linguagem de máquina COMO OS COMPUTADORES EXECUTAM SUAS INSTRUÇÕES Considere a arquitetura simplificada de um computador apresentada na figura 1. Nela, apresentamos o processador com três registradores (R0, R1 e R2), os barramentos de dados e endereços e a memória que possui 8 células, numeradas de 0 a 7. ATENÇÃO Vários componentes como registrador de instruções, contador de instruções unidade aritmética e lógica e outros componentes foram intencionalmente omitidos para focarmos apenas nos que são interessantes em nossa explicação. Fonte: O autor, 2020. Figura 1 - Arquitetura simplificada. Imagine agora que você deseja executar a seguinte operação: A := B + C Atenção! Para visualização completa da equação utilize a rolagem horizontal Onde A, B e C são variáveis mapeadas respectivamente para as posições 5, 6 e 7 da memória. Considere ainda que B e C possuem os valores 10 e 8 e já estão armazenados na memória, conforme será apresentado na Figura 2. ATENÇÃO Para fins meramente didáticos, os valores são apresentados em decimal e não em binário, como efetivamente, ficam na memória. Fonte: O autor, 2020. Figura 2 - B e C na memória. Um processador executa comando em linguagem de máquina cujos comandos podem ser, de forma simplificada, divididos em código de operação e um ou mais endereços (Figura 3), constituindo, cada um deles, um conjunto de bits. Fonte: O autor, 2020. Figura 3 - Formato de instrução. Para continuarmos nosso exemplo, vamos considerar que o processador possui as seguintes instruções: Nr Instrução Código de operação 1 Ler um dado da memória para um registrador. 00 2 Somar um dado de um registrador com o R0 armazenando o resultado em R0. 01 3 Gravar o dado de um registrador em uma posição de memória. 10 Atenção! Para visualização completa da tabela utilize a rolagem horizontal A partir deste conjunto de instruções, o programa seria então traduzido como: 1 LER B para R0 2 LER C para R1 3 SOMAR R0 a R1 4 GRAVAR R0 em A Em linguagem de máquina, iria corresponder: Instrução Linguagem Máquina Observação LER B para R0 00 110 000 Onde 00 é o código da operação, 110 a posição de memória 6, que corresponde à B e 000 o registrador R0. LER C para R1 00 111 001 Onde 00 é o código da operação, 111 a posição de memória 7, que corresponde à C e 001 o registrador R1. SOMAR R0 a R1 01 001 Onde 01 é o código da operação e 001 corresponde ao registrador 1. GRAVAR R0 em A 10 000 101 Onde 10 é o código da operação, 000 o registrador R0 e 101 a posição de memória 5, que corresponde à A. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Após execução, as posições memória e os registradores estariam com os valores mostrados na Figura 4. Fonte: O autor, 2020. Figura 4 - Situação após a execução dos comandos. MONTADORES E LINGUAGEM DE MONTAGEM Conforme observamos na seção anterior, uma simples operação de alto nível gera um conjunto de instruções em linguagem de máquina. Se voltarmos no tempo para a época dos primeiros computadores, poderíamos observar que eles programavam diretamente em linguagem de máquina, ou seja, o programa nada mais era que uma sequência enorme de 0 e 1. Isto, obviamente, dificultava muito o trabalho, pois a possibilidade de escrever um comando errado e a dificuldade de fazer a correção eram enormes. Surgiu então, a primeira ideia de facilitar o trabalho, criar uma linguagem de programação de baixo nível, que se aproxime da lógica da máquina e que mantenha uma relação de 1 para 1, ou seja, um comando na linguagem irá corresponder a um único comando em linguagem de máquina. Fonte: Shutterstock. A partir desta ideia, foram criados mnemónicos, ou seja, conjuntos de letras para representar as instruções. Voltando para a linguagem de nosso processador hipotético, poderíamos associar os mnemónicos às instruções, conforme a tabela abaixo: Nr Instrução Mnemónico 1 Ler um dado da memória para um registrador. LOAD 2 Somar um dado de um registrador com o R0 armazenando o ADD resultado em R0. 3 Gravar o dado de um registrador em uma posição de memória. MOV Atenção! Para visualização completa da tabela utilize a rolagem horizontal Além disso, o endereçamento passou a ser feito por nomes lógicos e não mais pelo endereço do registrador ou da memória. Dessa forma, o programa anterior ficaria assim: Instrução Linguagem de máquina Linguagem de baixo nível LER B para R0 00 110 000 LOAD B R0 LER C para R1 00 111 001 LOAD C R1 SOMAR R0 a R1 01 001 ADD R1 GRAVAR R0 em A 10 000 101 MOV RO A Atenção! Para visualização completa da tabela utilize a rolagem horizontal ESTE TIPO DE LINGUAGEM É DENOMINADO LINGUAGEM DE MONTAGEM, OU ASSEMBLY, E É INTIMAMENTE ASSOCIADO À ARQUITETURA DE UM DETERMINADO PROCESSADOR. Muito bem, dado o programa: 1 LOAD B R0 2 LOAD C R1 3 ADD R1 4 MOV RO A APÓS ANALISAR O CÓDIGO ACIMA, É POSSÍVEL AFIRMAR QUE ELE IRÁ EXECUTAR DIRETAMENTE? RESPOSTA Nada disso! O processador continua apenas entendendo a linguagem de máquina e precisamos então fazer a tradução do assembly para a linguagem de máquina. É aqui que surge o programa tradutor, chamado montador. Montador, também denominado assembler, é um programa que traduz para linguagem de máquina a linguagem de baixo nível. O processo de montagem está ilustrado na Figura 5. Fonte: O autor, 2020. Figura 5 - Processo de montagem. ESSA TRADUÇÃO É RELATIVAMENTE SIMPLES, JÁ QUE A LINGUAGEM MANTÉM UMA RELAÇÃO DE 1 PARA 1 COM AS INSTRUÇÕES DO PROCESSADOR E, DURANTE ESTE PROCESSO, O MONTADOR FARÁ TAMBÉM UMA VERIFICAÇÃO DA CORREÇÃO DA ESCRITA DO CÓDIGO, OBSERVANDO SE NÃO EXISTEM ERROS DE DIGITAÇÃO DE COMANDO ETC. Além da tradução dos mnemônicos, o montador precisa ainda lidar com os nomes de variáveis que precisam ser mapeadas para posições de memória. Na realidade, o processo de montagem acontece, normalmente, em dois passos: 1 Definição de símbolos armazenados em uma tabela. 2 Tradução do programa utilizando a tabela do passo 1. PASSO 1 No passo 1, o montador faz uma primeira passagem pelo código e levanta os símbolos existentes a partir da análise das operações existentes no programa e cria uma tabela de símbolos cujas entradas possuem, resumidamente, dois campos: • Símbolo – contém o próprio identificador do símbolo, por exemplo A, B ou C do nosso exemplo. • Posicionamento – localização do símbolo em relação ao ponto de início de carga do programa na memória. PASSO 2 No passo dois, utilizando asinformações geradas, é realizada efetivamente a tradução, onde ocorrem as seguintes operações: • São gerados os códigos de máquina correspondentes às instruções. • São gerados relatórios de erros, se houver. • São produzidas as informações necessárias ao processo de ligação. LIGAÇÃO E CARGA A tradução foi realizada, agora vai direto para a execução? RESPOSTA Não, ainda temos dois passos: a ligação e a carga. Um programa, normalmente, é composto de vários procedimentos, que nada mais são que objetos independentes que necessitam ser associados como um único programa. PARTE 1 Quem faz esta ligação é o ligador (linker), que une os diversos procedimentos em um único módulo de carga. PARTE 2 O módulo de carga é o programa executável que será carregado na memória pelo carregador, quando, finalmente, o programa será executado. A Figura 6 mostra o processo completo de tradução e carga em uma linguagem de montagem. Fonte: O autor, 2020. Figura 6 - Processo de montagem e carga. LINGUAGEM DE ALTO NÍVEL Conforme vimos, o assembly é denominado como uma linguagem de baixo nível, pois sua lógica de programação se aproxima da lógica de execução de programas da máquina. Este tipo de linguagem apesar de ser um avanço em relação à programação diretamente em linguagem de máquina, apresenta alguns inconvenientes para o desenvolvimento de sistemas complexos. Se vocês se lembram, para realizar aquela simples operação do exemplo A := B + C foram necessários quatro comandos de assembly: 1 LOAD B R0 2 LOAD C R1 3 ADD R1 4 MOV RO A Porém, pela lógica humana, seria apenas um comando. Dessa forma, ao longo dos anos, os projetistas de linguagem de programação (LP) desenvolveram uma solução que denominamos linguagem de alto nível, visando facilitar a implementação de sistemas, já que estas LP’s possuem uma lógica de programação similar a nossa e não a da máquina. Imagine um programa que você desejasse ler dois valores digitados por um usuário, somá-los e exibir o resultado na tela. Em termos de algoritmos, este programa seria: VARIÁVEIS A, B, C INTEIRO INÍCIO EXIBA (“B:”); RECEBA B; EXIBA (“C:”); RECEBA C; A := B + C EXIBA (“B + C = “, A) FIM Dá para imaginar que este pequeno código para ser escrito em linguagem de baixo nível implicaria na manipulação de buffer de entrada e saída, leituras e escrita na memória etc. Já em qualquer linguagem de alto nível, como C, Pascal, Java, seria praticamente igual ao algoritmo. Veja a listagem abaixo referente ao mesmo programa escrito em C. 1 #include 2 3 int main(void) 4 { 5 int a, b, c; 6 printf("b: "); 7 scanf("%d", &b); 8 printf("c: "); 9 scanf("%d", &c); 10 a = b + c; 11 printf("b + c = %d\n", a); 12 return 0; 13 } Listagem 1 – Programa de soma em C. ATENÇÃO Neste código, podemos notar a presença de funções de biblioteca (printf e scanf) e a inclusão da biblioteca que os contêm (stdio.h). Lembram da ligação? É necessário por causa do uso de bibliotecas, pois o código desta tem que ser anexado ao código do programa. A tradução deste tipo de programa é bem mais complexa que a realizada pelo montador, pois um único comando pode implicar e, quase com certeza, implica na geração de várias linhas de comando em linguagem de máquina. Existem basicamente três tipos de tradutores para linguagens de alto nível: INTERPRETADORES COMPILADORES HÍBRIDOS Cada um possui vantagens e desvantagens. Vamos a eles. INTERPRETADORES INTERPRETADORES SÃO TRADUTORES QUE FAZEM A CONVERSÃO DO PROGRAMA FONTE COMANDO A COMANDO. ELES NÃO GERAM O CÓDIGO OBJETO, DE CERTA FORMA, É COMO SE O CÓDIGO FONTE FOSSE O OBJETO, JÁ QUE SERÁ EXECUTADO DIRETAMENTE. No processo de interpretação, o código fonte é traduzido linha a linha, fazendo as análises sintática e semântica e, se não houver erro, o comando é executado. Após a execução bem sucedida de um comando, é buscado o próximo (Figura 7). Fonte: O autor, 2020. Figura 7 - Processo de interpretação. ATENÇÃO Como o programa vai sendo executado à medida em que vai sendo traduzido, sem geração de código objeto, a cada execução o programa necessita ser novamente interpretado. O interpretador pode ser visto como uma virtualização da máquina hospedeira que consegue lidar diretamente com a linguagem de alto nível, ao contrário do hardware real, que lida com linguagem de máquina. A interpretação possui as seguintes vantagens e desvantagens: VANTAGENS Como cada instrução é traduzida individualmente, se ocorrer um erro, a correção é fácil, pois é rápido identificar onde ele ocorreu. O código não precisa ser compilado antes da execução. Qualquer alteração no programa fonte será interpretada normalmente, não exigindo qualquer tratamento. Atenção! Para visualização completa da tabela utilize a rolagem horizontal DESVANTAGENS A execução do programa é bem mais lenta, pois é necessário primeiro fazer a tradução. O código fonte tem que estar disponível, pois é a partir deste que a interpretação e posterior execução é realizada. Se ocorrer um erro de tradução, a execução do programa é interrompida. Atenção! Para visualização completa da tabela utilize a rolagem horizontal ALÉM DE LINGUAGEM DE PROGRAMAÇÃO, COMO LISP E BASIC, QUE SÃO INTERPRETADAS, OS COMANDOS DE SHELL SCRIPT DE SISTEMAS OPERACIONAIS, BEM COMO VÁRIAS LINGUAGENS DA WEB, COMO PHP, UTILIZAM ESTE TIPO DE TRADUÇÃO. Devido às características apresentadas, a execução de um programa interpretado é concomitante a sua tradução. Isto é, elas ocorrem no mesmo movimento, conforme ilustrado na Figura 8, não havendo um movimento de tradução inicial, como vimos no montador e, posteriormente, o movimento de execução do programa. Fonte: O autor, 2020. Figura 8 - Execução de um programa por um interpretador. COMPILADORES Um compilador é um programa tradutor que faz a transformação do programa fonte em um programa executável que pode ser carregado na memória e executado (Figura 9). Fonte: O autor, 2020. Figura 9 - Geração de executável com a compilação. Como o código executável é totalmente dependente da arquitetura do sistema, onde será executado, implica que o programa compilado para um determinado ambiente computacional não irá executar em outro. Dessa forma, o programa deve ser recompilado se desejarmos portá-lo para um computador com arquitetura diferente. Na compilação, temos então dois movimentos distintos: 1 A geração do programa executável pelo processo de compilação. 2 A carga e a execução do programa que pode ser realizado muito tempo depois da compilação. A compilação possui as seguintes vantagens e desvantagens: VANTAGENS A execução do código compilado é mais rápida que o do interpretado. Somente o código executável é necessário para rodar o programa, o que mantém a confidencialidade do código fonte original. O código pode ser otimizado pelo compilador. O executável somente é gerado se não houver nenhum erro no código fonte, dessa forma, o programa somente será abortado em tempo de execução por erros de execução, mas nunca por erros de tradução. Atenção! Para visualização completa da tabela utilize a rolagem horizontal DESVANTAGENS Para ser executado, o código fonte necessita passar por todo o processo de compilação, o que pode levar um tempo considerável. A depuração do código e correção de erros é mais complexa, já que é gerado um relatório final com todos os erros da compilação, o que torna a localização do erro mais difícil e a verificação de sua correção mais demorada, pois o programa deverá ser novamente todo compilado. Qualquer alteração na lógica do programa exigirá uma nova compilação. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Exemplos de linguagem que usam compilação são: C, C++, Pascal, ADA, Cobol. TRADUTORES HÍBRIDOS Conforme vimos nas seções anteriores, compilação e interpretação possuem abordagens bem distintas em relação à forma de gerar os comandos de máquina: COMPILAÇÃO INTERPRETAÇÃOCOMPILAÇÃO Na compilação, é gerado um executável para uma determinada arquitetura computacional que, se tiver que ser portado para outra deverá passar por um novo processo de tradução. INTERPRETAÇÃO Na interpretação, o código do programa a ser executado é sempre o mesmo, pois está em linguagem de alto nível, o que muda de uma arquitetura computacional para outra é o interpretador. Ou seja, para executar o programa em uma arquitetura A, utilizamos um interpretador, para executar em outra arquitetura B, outro tradutor adequado terá que ser empregado. CADA UMA DESSAS ABORDAGENS POSSUI VANTAGENS E DESVANTAGENS, O QUE LEVOU ALGUNS PROJETISTAS DE LINGUAGENS DE PROGRAMAÇÃO A UTILIZAR UMA ABORDAGEM HIBRIDA, JUNTANDO UMA COMPILAÇÃO INICIAL SEGUIDA POR UM PROCESSO DE INTERPRETAÇÃO. Linguagens como Java ou Python são exemplos desta abordagem. Vejamos com mais detalhes a solução Java. JAVA Os ambientes de desenvolvimento Java incluem um compilador que não traduz o código fonte diretamente para a linguagem de máquina, na realidade, a tradução é realizada para um código intermediário constituído de bytecodes, que são a Linguagem da Máquina Virtual Java (JVM). Em uma implementação simples, a JVM funciona com um interpretador a partir do código intermediário (bytecode), realizando a sua execução (Figura 10). Fonte: O autor, 2020. Figura 10 - Execução de programa com a JVM com interpretador. Com a evolução da tecnologia, as JVM passaram a incluir um compilador interno que traduz o bytecode para a linguagem nativa do computador onde ela está instalada. Isso foi chamado de compilação Just in Time, ou JIT e visa melhorar o desempenho do programa em tempo de execução (Figura 11). ATENÇÃO Embora, atualmente, o código Java utilize JIT para obter uma maior velocidade, os applets Java são descarregados do servidor na forma de bytecode. Fonte: O autor, 2020. Figura 11 - Execução de programa com a JVM com JIT. Cabe observar que, mesmo os interpretadores reais, utilizam algum tipo de tradução intermediária para facilitar a execução, evitando a perda de tempo de interpretar instrução a instrução no momento de cada tradução. Também é muito comum que linguagens compiladas para código nativo de uma máquina tenham um runtime acoplado (sistema de tempo de execução) que se destine ao suporte durante a execução, o que, de certa forma, insere algum nível de interpretação. Portanto, é muito difícil em casos reais fazer uma total separação de interpretação e compilação, na realidade, as duas utilizam técnicas parecidas para a tradução e muitas vezes, conforme vimos, atuam efetivamente juntas. RUNTIME Refere-se ao ambiente de execução que um determinado programa irá executar e não deve ser confundido com o ciclo de vida de execução de um programa, quando o ambiente de execução estará em operação. javascript:void(0) A EVOLUÇÃO DO PROCESSO DE TRADUÇÃO VERIFICANDO O APRENDIZADO 1. OS COMPUTADORES COMPREENDEM APENAS BITS, PORTANTO, É NECESSÁRIO QUE EXISTA UM TRADUTOR QUE TRANSFORME O CÓDIGO FONTE CRIADO PELO PROGRAMADOR PARA UMA SEQUÊNCIA QUE POSSA SER ENTENDIDA PELOS PROCESSADORES. PARA O JAVA, É UTILIZADO UM MÉTODO DE TRADUÇÃO HÍBRIDO ENVOLVENDO, INICIALMENTE, UMA COMPILAÇÃO E, DEPOIS, UMA INTERPRETAÇÃO. EM JAVA, O PRODUTO DA FASE DE COMPILAÇÃO DENOMINA-SE: A) Código fonte. B) Código objeto. C) Executável. D) ByteCode. 2. OS DIVERSOS PROCESSOS DE TRADUÇÃO POSSUEM CARACTERÍSTICAS PRÓPRIAS QUE IRÃO DEFINIR EM QUE SITUAÇÃO ELES SÃO MELHORES APLICADOS, DE ACORDO COM AS VANTAGENS E DESVANTAGENS DE CADA MÉTODO. A INTERPRETAÇÃO POSSUI VÁRIAS DESVANTAGENS EM RELAÇÃO À COMPILAÇÃO, UMA DELAS É: A) A identificação e correção de erros é dificultada. B) Alterações no programa fonte exigem uma nova tradução. C) A execução é mais lenta que o programa compilado. D) O Programa precisa ser instalado na máquina para permitir sua execução. GABARITO 1. Os computadores compreendem apenas bits, portanto, é necessário que exista um tradutor que transforme o código fonte criado pelo programador para uma sequência que possa ser entendida pelos processadores. Para o Java, é utilizado um método de tradução híbrido envolvendo, inicialmente, uma compilação e, depois, uma interpretação. Em Java, o produto da fase de compilação denomina-se: A alternativa "D " está correta. Ao contrário da compilação pura, em que, a partir do código fonte, é gerado um código objeto a ser submetido ao montador para a tradução da linguagem de máquina em JAVA, a fase de compilação gera um formato intermediário a ser passado ao JVM, que é o interpretador para execução. Esse formato intermediário denomina-se Bytecode. 2. Os diversos processos de tradução possuem características próprias que irão definir em que situação eles são melhores aplicados, de acordo com as vantagens e desvantagens de cada método. A interpretação possui várias desvantagens em relação à compilação, uma delas é: A alternativa "C " está correta. De fato, como a tradução é realizada à medida que o programa vai sendo executado, na realidade, o tempo de processamento é maior que o do programa compilado. MÓDULO 2 Identificar os passos do processo de compilação e de seus produtos Conforme vimos, um compilador faz a tradução do código fonte para um código executável. Este processo, na realidade, pode ser dividido em três fases: a primeira realizada pelo compilador propriamente dito, a segunda por um montador e a terceira por um ligador, sendo que, na maioria das vezes, estes dois últimos são parte integrante do próprio software de compilação, porém, executando atividades específicas. ANALISANDO A FIGURA 12 PODEMOS VER QUE O COMPILADOR GERA UM ARQUIVO EM LINGUAGEM DE BAIXO NÍVEL (CÓDIGO INTERMEDIÁRIO) CORRESPONDENDO A FASE 1. A SEGUIR ESTE CÓDIGO SERÁ TRANSFORMADO EM CÓDIGO OBJETO PELO MONTADOR, CORRESPONDENDO A FASE 2 E, NA SEQUÊNCIA, O LIGADOR FAZ A ASSOCIAÇÃO DOS CÓDIGOS DE BIBLIOTECA GERANDO O EXECUTÁVEL EM LINGUAGEM DE MÁQUINA PERFAZENDO A FASE 3. A partir da criação do executável, o usuário pode comandar a sua execução quando desejar, momento em que o programa será carregado na memória pelo carregador. Fonte: O autor, 2020. Figura 12 - Geração de executável pelo compilador de montador. ETAPAS DA COMPILAÇÃO A atividade do compilador, propriamente dita, pode ser dividida em duas etapas bem distintas (Figura 13): ANÁLISE OU FRONT END SÍNTESE OU BACK END ANÁLISE OU FRONT END A partir da gramática de uma determinada linguagem de programação, visa verificar se o código fonte foi escrito conforme as regras de sintaxe da linguagem e se está livre de erros, gerando como produto uma representação intermediária. SÍNTESE OU BACK END Utilizando otimizações e gerenciando a tabela de símbolos, visa criar o melhor código intermediário a ser submetido ao montador. Fonte: O autor, 2020. Figura 13 - Etapas da compilação. ANÁLISE OU FRONT END A etapa de análise é dividida em três passos: ANÁLISE LÉXICA Converte o texto do programa fonte em um fluxo de palavras (tokens). ANÁLISE SINTÁTICA Determina se a sequência de tokens gerados no passo anterior corresponde a uma sentença na linguagem-fonte gerando uma árvore sintática. ANÁLISE SEMÂNTICA Neste passo, é analisado o contexto do programa, incluindo a verificação dos tipos das variáveis utilizadas no programa fonte e a depuração de outros erros semânticos, sendo então gerada a representação intermediária a ser utilizada na fase de Back End. A Figura 14 mostra os passos desta etapa: Fonte: O autor, 2020. Figura 14 - Passos da análise. Na Figura 14, vemos que o resultado é a Representação Intermediária, vejamos a que isso se refere. REPRESENTAÇÃO INTERMEDIÁRIA (IR) O último passo da etapa de análise implica na geração de algum tipo de representação intermediária do código fonte. ATENÇÃO Vários formatos de IR podem ser utilizados pelos compiladores, dependendo da linguagem de programação utilizada no programa fonte,da linguagem alvo para a qual se deseja traduzi-lo e das transformações que o compilador utiliza. Algumas representações utilizam um grafo, outras se parecem com um programa escrito em assembly sequencial. Veja o código abaixo: 1 t0 ← a x 2 2 t1 ← t0 x b; 3 t2 ← t1 x c 4 t3 ← t2 x d 5 a ← t3 Ele seria uma possível representação intermediária para a seguinte expressão em linguagem de alto nível: A ← A X 2 X B X C X D Atenção! Para visualização completa da equação utilize a rolagem horizontal O NOSSO EXEMPLO FOI MUITO SIMPLES, POIS A SUA REPRESENTAÇÃO FOI BASICAMENTE SEPARAR AS DIVERSAS OPERAÇÕES DA EXPRESSÃO EM EXPRESSÕES MAIS SIMPLES QUE GERAM RESULTADOS INTERMEDIÁRIOS. PORÉM, LINGUAGENS REAIS POSSUEM CONSTRUÇÕES MUITO MAIS COMPLEXAS (IF, LOOP, ETC) E O COMPILADOR NECESSITA TER UMA ESTRATÉGIA PARA GERAR O IR PARA CADA UMA DELAS. LINGUAGENS E GRAMÁTICAS Na etapa de análise, as verificações, particularmente, a léxica e a sintática, são dependentes da gramática específica da linguagem de programação que está sendo traduzida. Fonte: Shutterstock. Existe toda uma teoria por detrás do desenvolvimento de linguagens de programação baseado nos conceitos de linguagens formais. Alguns conceitos fundamentais que temos que ter são: LINGUAGEM SÍMBOLO SENTENÇA ALFABETO LINGUAGEM O conjunto de símbolos e de regras que determinam como se pode combinar esses símbolos em sentenças corretas do ponto de vista sintático. SÍMBOLO É uma entidade abstrata, por exemplo, as letras e os dígitos. Do ponto de vista léxico, eles são ordenáveis, podendo ser comparáveis quanto à igualdade ou à precedência. São utilizados, principalmente, como elementos atômicos em linguagens. SENTENÇA Também denominada palavra, constitui de uma sequência finita de símbolos. ALFABETO É um conjunto finito de símbolos. Por exemplo, o alfabeto das vogais {a, e, i, o, u}. A partir desses conceitos básicos, podemos definir que uma linguagem L é um conjunto de sentenças formadas a partir dos símbolos de um determinado alfabeto V. Uma linguagem pode ser: FINITA: Quando é composta por um conjunto finito de sentenças. INFINITA: Quando é composta por um conjunto infinito de sentenças. Uma linguagem pode ser definida por uma descrição algébrica e, para verificar se uma sentença pertence a uma linguagem, podemos utilizar os reconhecedores que podem ser implementados em autômatos finitos, autômatos de pilha ou máquinas de Turing. Já para gerar, sistematicamente sentenças de uma linguagem, usamos os geradores que tem como exemplo as gramáticas. As linguagens segundo Chomsky podem ser classificadas como: Linguagem Regular (ou tipo 3) Linguagem Livre de Contexto (ou tipo 2) Linguagem Sensível ao Contexto (ou tipo 1) Linguagem Irrestrita (ou tipo 0) ATENÇÃO Sendo que uma linguagem do tipo 3 é também do tipo 2, mas nem toda linguagem do tipo 2 é do tipo 3. Isso acontece de forma recursiva, ou seja, uma do tipo 2 é do tipo 1, mas a recíproca não é verdadeira, e assim sucessivamente. As linguagens do tipo 3 geram as chamadas expressões regulares que podem ser processadas por autômatos finitos e permitem a criação de analisadores léxicos. Já as de tipo 2 são adequadas à criação de linguagens de programação, isso acontece devido ao fato das linguagens livre de contexto e sua gramática (Gramática Livre de Contexto - GLC) permitirem a especificação adequada das estruturas sintáticas, como: construções aninhadas, parêntesis balanceado etc. Elas são processadas por autômatos de pilha e permitem a eficiente criação de analisadores sintáticos. VOCÊ PODE ESTAR SE PERGUNTANDO, O QUE É UMA GRAMÁTICA? RESPOSTA Uma gramática define regras para a combinação de símbolos de um alfabeto, de forma que apenas determinadas combinações de símbolos formem uma sentença válida. Uma gramática G é definida como uma quádrupla G = (N, T, P, S), onde: N é um conjunto de símbolos não-terminais, usados na descrição da linguagem. T é um conjunto de símbolos terminais, constituindo os símbolos da linguagem propriamente ditos. P define as regras de produção (ou gramaticais) para a combinação dos símbolos da linguagem em pares (a, b). S é o símbolo inicial da gramática pertencente à N. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Vejamos um exemplo de uma gramática que especifica a linguagem dos números inteiros sem sinal: G = (N, T, P, S) Atenção! Para visualização completa da equação utilize a rolagem horizontal Onde: N T = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } P = { N → D N | D D → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 } S = N Atenção! Para visualização completa da equação utilize a rolagem horizontal SÍNTESE OU BACK END A etapa de síntese é dividida em 3 passos: GERAÇÃO DE CÓDIGO INTERMEDIÁRIO OTIMIZAÇÃO DE CÓDIGO GERAÇÃO DE CÓDIGO GERAÇÃO DE CÓDIGO INTERMEDIÁRIO A partir da representação intermediária gerada na etapa de análise gera uma sequência de código que funciona em uma máquina abstrata com duas propriedades importantes: • Deve ser fácil de ser produzido. • Deve ser fácil de traduzir para a linguagem alvo no código de montagem. OTIMIZAÇÃO DE CÓDIGO Realiza a otimização do código buscando uma melhor utilização da memória e diminuição do tempo de execução. GERAÇÃO DE CÓDIGO É o último passo que finalmente gera o código de montagem. A Figura 15 mostra os passos da síntese: Fonte: O autor, 2020. Figura 15 - Passos da síntese. Dessa forma, o processo de compilação pode ser resumido, como apresentado na Figura 16. Fonte: O autor, 2020. Figura 16 - Processo completo de compilação. Observando a Figura 16, podemos notar que apareceram dois novos elementos: tratamento de erros e tabelas. Vamos ver a que eles se referem. TRATAMENTO DE ERROS Em todos os passos da etapa de análise (Léxica, Sintática e Semântica), deve ser possível detectar erros no programa fonte que está sendo compilado. Logicamente, esses erros devem ser tratados, porém, ao contrário da interpretação, o erro ao ser detectado não pode interromper o processo de compilação. Portanto, a etapa de análise deve continuar até o fim e, então, gerar um relatório com todos os erros detectados. Isto é realizado a partir de mecanismos de recuperação de erros, encarregados de evitar que um erro inicial se propague para outros pontos do programa fonte. A recuperação é conseguida ao se ressincronizar o passo da análise que está sendo processada com o ponto do texto do programa fonte onde foi detectado o erro, ou seja, é fundamental que o compilador prossiga com a tradução após detectar algum erro, evitando sua propagação e realizando a análise de todo código fonte. TABELAS As tabelas são estruturas de armazenamento e as rotinas que as manipulam. Elas são utilizadas em todos os passos da compilação. Temos tabelas constantes para cada linguagem de programação diferente, como: a tabela de palavras reservadas, de delimitadores, de operadores etc. ATENÇÃO No entanto, existe uma tabela que é gerada durante o processo de tradução, que possui importância fundamental: a tabela de símbolos. Esta tabela armazena os dados de: Declarações de variáveis Declarações dos procedimentos ou sub-rotinas Parâmetros de sub-rotinas Entre outros Atenção! Para visualização completa da tabela utilize a rolagem horizontal Quando é percebida a ocorrência de um identificador no programa fonte, a tabela é consultada à procura do identificador. Se ele existir, as informações da tabela são comparadas com as obtidas a partir da fonte, se algum dado novo for percebido ou o identificador não existir, ele será inserido na tabela. As informações a serem armazenadas dependem da linguagem fonte, da linguagem alvo da tradução e do projeto do compilador. Entretanto, comumente, são registrados os seguintes atributos: PARA VARIÁVEIS Classe (var), tipo, endereço no texto, precisão, tamanho. PARÂMETROS FORMAIS Classe (par), tipo, mecanismo de passagem. PROCEDIMENTOS/SUB-ROTINASClasse (proc), número de parâmetros. A ESTRUTURA DA TABELA DE SÍMBOLOS DEVE PERMITIR UMA MANIPULAÇÃO RÁPIDA E EFICIENTE, TANTO PARA A INSERÇÃO QUANTO PARA A CONSULTA E DEVE AINDA SER O MAIS COMPACTA POSSÍVEL. javascript:void(0) javascript:void(0) javascript:void(0) EXEMPLO DO FUNCIONAMENTO DE UM COMPILADOR Para exemplificar o processo de compilação, vamos considerar que desejamos traduzir a seguinte expressão que calcula o perímetro de um trapézio. PERÍMETRO := LADO 1 + LADO2 + LADO3 + LADO4 Atenção! Para visualização completa da equação utilize a rolagem horizontal Inicialmente, devemos converter os elementos da expressão em variáveis associadas. Utilizaremos então a seguinte associação: ELEMENTO VARIÁVEL Perímetro A Lado1 B Lado2 C Lado3 D Lado4 E Atenção! Para visualização completa da tabela utilize a rolagem horizontal Nossa expressão ficaria então: A ← B + C + D + E; Atenção! Para visualização completa da equação utilize a rolagem horizontal O primeiro passo do processo será a Análise Léxica que, a partir da tabela de símbolos da linguagem, identificará os tokens que, neste caso, seriam os da Figura 17. Fonte: O autor, 2020. Figura 17 - Tokens gerados. ATENÇÃO Espaços em branco, marcadores de final de linha e demais elementos que não interessam para tradução, como comentários, são ignorados. O segundo passo é a Análise Sintática, onde será verificado se as variáveis do programa (A, B, C, D e E) são reconhecidas. Para isso, será consultada a tabela interna de símbolos ou as declarações explícitas do código. A seguir, serão verificas as expressões “←” e “+” e o seu significado no contexto da linguagem de programação do código fonte. A saída seria uma árvore de execução, como a da Figura 18. Fonte: O autor, 2020. Figura 18 - Arvore sintática. O terceiro passo, o analisador semântico irá transformar a árvore sintática em algum tipo de representação intermediária que a represente de uma forma que possa ser entendida por uma máquina. Em nosso exemplo, esta representação poderia ser a exibida na Figura 19, onde T1, T2 e T3 representam resultados intermediários. Fonte: O autor, 2020. Figura 19 – Representação intermediária. ATENÇÃO Terminada a etapa de análise, o compilador começa a síntese com o passo onde será produzido o código de montagem. Inicialmente, ele irá procurar criar um código intermediário que possa ser executado por uma máquina abstrata genérica. Por exemplo, a representação intermediaria da Figura 19 poderia gerar o código intermediário da Figura 20. Fonte: O autor, 2020. Figura 20 - Código intermediário. A seguir, ele tentará otimizar o código, por exemplo, poderia reduzir um passo armazenando diretamente em A o resultado da soma de B com T2, como pode ser visto na Figura 21. Fonte: O autor, 2020. Figura 21 - Código otimizado. Finalmente, seria gerado o código em linguagem de montagem, considerando os comandos vistos no módulo 1, o código poderia ser o presente na Figura 22: Fonte: O autor, 2020. Figura 22 - Código de montagem. A Figura 23 mostra todos os passos da compilação e seus produtos: Fonte: O autor, 2020. Figura 23 - Passos da compilação e seus produtos. AS ETAPAS DO PROCESSO DE COMPILAÇÃO VERIFICANDO O APRENDIZADO 1. O PROCESSO DE TRADUÇÃO ENVOLVE DIVERSAS ETAPAS, COMO AS ANÁLISES LÉXICA, SINTÁTICA E SEMÂNTICA. CADA UMA DESTAS ETAPAS, NORMALMENTE, PRODUZ UMA REPRESENTAÇÃO, QUE SERÁ A ENTRADA PARA A PRÓXIMA ETAPA. CONSIDERANDO QUE VOCÊ DESEJA REALIZAR A OPERAÇÃO A := B + C OBSERVE AGORA A SEGUINTE REPRESENTAÇÃO: ELA CORRESPONDE AO PRODUTO DO PASSO: A) Análise Léxica. B) Análise Sintática. C) Análise Semântica. D) Geração de Código. 2. A COMPILAÇÃO PODE SER DIVIDIDA EM DUAS ETAPAS DISTINTAS: FRONT END OU ANÁLISE E BACK END OU SÍNTESE. CADA UMA DESTAS ETAPAS É DIVIDIDA EM PASSOS QUE SÃO NECESSÁRIOS PARA A GERAÇÃO DO CÓDIGO FINAL. UM DOS PASSOS DA ETAPA DE BACK END É: A) Análise Semântica. B) Otimização. C) Análise Sintática. D) Montagem. GABARITO 1. O processo de tradução envolve diversas etapas, como as Análises Léxica, Sintática e Semântica. Cada uma destas etapas, normalmente, produz uma representação, que será a entrada para a próxima etapa. Considerando que você deseja realizar a operação A := B + C observe agora a seguinte representação: Ela corresponde ao produto do passo: A alternativa "B " está correta. Os produtos de cada umas das fases listadas é: • Análise Léxica - lista de tokens • Análise Sintática - árvore sintática • Análise Semântica – representação intermediária • Geração de Código – código de montagem No caso, a representação apresentada corresponde a uma árvore sintática, portanto, é o produto da Análise Sintática. 2. A compilação pode ser dividida em duas etapas distintas: Front End ou Análise e Back End ou Síntese. Cada uma destas etapas é dividida em passos que são necessários para a geração do código final. Um dos passos da etapa de Back End é: A alternativa "B " está correta. O processo de tradução realizado pelo compilador envolve duas etapas: Análise também chamada de Front End e Síntese denominada também Back End. A etapa de Síntese possui 3 passos: • Geração de Código Intermediário • Otimização • Geração de Código ou Geração de Código de Montagem Portanto, a resposta é Otimização. Convém observar ainda que a montagem é realizada pelo montador a partir do Código de Montagem gerado pelo compilador. MÓDULO 3 Analisar a estrutura funcional do compilador e de seus componentes Conforme vimos no módulo anterior, os passos do compilador são executados por componentes específicos: PASSO 1 Analisador Léxico PASSO 2 Analisador Sintático PASSO 3 Analisador Semântico PASSO 6 Gerador de Código de Montagem PASSO 5 Otimizador PASSO 4 Gerador de Código Intermediário PASSO 1 Analisador Léxico PASSO 2 Analisador Sintático PASSO 3 Analisador Semântico PASSO 4 Gerador de Código Intermediário PASSO 5 Otimizador PASSO 6 Gerador de Código de Montagem Vamos a partir de agora estudar com mais detalhes cada um deles, mas antes de passarmos a este estudo, vamos definir um Linguagem de Programação de Exemplo (LPE) a ser utilizada para o entendimento do funcionamento dos diversos componentes. LINGUAGEM DE PROGRAMAÇÃO DE EXEMPLO (LPE) - DEFINIÇÃO LÉXICA A nossa LPE possui sua definição léxica estabelecida na notação BNF (Backus Naur Form). Esta notação utiliza regras de derivação na forma: < SÍMBOLO > : : = < EXPRESSÃO COM SÍMBOLOS > Atenção! Para visualização completa da equação utilize a rolagem horizontal Onde <símbolo> é um não terminal e a expressão é um conjunto de símbolos. Por exemplo, considere essa BNF possível para um aluno: EXEMPLO <CLIENTE> ::= <NOME> <ENDEREÇO> <NOME> ::= <PRIMEIRO NOME> <ÚLTIMO-NOME> <FDL> <ENDEREÇO> ::= <NOME-DA-RUA> <NÚMERO-DO- IMÓVEL> <NUM-APTO-OPC> <PARTE-DO-CEP> <FDL> <PARTE-DO-CEP> ::= <CEP> <NOME-DA-CIDADE> "-" <CÓDIGO-DO-ESTADO> <FDL> Isto se traduz para o português como: Um cliente consiste no nome, seguido pelo endereço. O nome consiste no primeiro nome seguido pelo último nome, seguido pelo fim de linha (FDL). Um endereço consiste no nome da rua, seguido pelo número do imóvel, seguido do especificador de apartamento opcional, seguido por um fim-da-linha. A parte do CEP consiste no CEP, seguido pelo nome da cidade, por um hífen, pelo código do Estado, e por um fim-da-linha. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Em nossa LPE a sua especificação seria então: IDENTIFICADORES PALAVRAS RESERVADAS TIPOS OPERADORES DELIMITADORES TERMINADOR DE COMANDO IDENTIFICADORES Devem começar com uma letra e depois podem ser seguidos por letras ou dígitos Identificador ::= {L} + {L}|{D} L ::= A..Z a..z D ::= 0..9 Observe na definição o seguinte: • { } - explicita que o termo envolvido pode ser repetido n vezes (se o termo estiver entre [ ] ele é opcional).• + - após um termo envolvido por {} indica que deve existir pelo menos uma ocorrência do termo. • I – permite a escolha entre os termos. • Os termos em negrito são os símbolos da linguagem. • Símbolos separados por .. indicam intervalo, ou seja, todos os valores entre os dois símbolos. PALAVRAS RESERVADAS Se Então Senão Inteiro Real String Programa TIPOS Inteiro ::= {D}+ Real ::= {D} + [.{D}+] | .{D} String ::= ‘{C}’ OPERADORES Operador Significado = Igual > Maior que < Menor que <> Diferente >= Maior ou igual <= Menor ou igual + Soma - Subtração / Divisão * Diferença := Atribuição Atenção! Para visualização completa da tabela utilize a rolagem horizontal DELIMITADORES ( ) [ ] { } , TERMINADOR DE COMANDO ; ANALISADOR LÉXICO O analisador léxico realiza o primeiro passo da etapa de análise da compilação. Ele serve de intermediário entre o programa fonte e o analisador sintático, já que este último faz chamadas ao léxico sempre que precisar reconhecer um novo token (Figura 24). Fonte: O autor. Figura 24 - Interação léxico/sintático. Embora as duas funções pudessem ser implementadas no analisador sintático, a sua separação traz vários benefícios: Simplificação do projeto do compilador, já que se faz a separação do tratamento da especificação dos símbolos da linguagem em relação a suas regras semânticas. Melhoria da eficiência pela aplicação de técnicas especializadas para a análise léxica. Melhor portabilidade devido ao tratamento mais fácil das mudanças que ocorram nos dispositivos de entrada do compilador. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Dessa forma, o analisador léxico faz a interface entre programa fonte e o analisador sintático, convertendo seu texto em uma sequência de tokens criando os lexemas. Mas o que são os tokens e os lexemas? TOKEN É um símbolo do alfabeto da linguagem, representando um padrão de caracteres, uma palavra reservada ou um identificador e irão constituir os dados de entrada para o processamento do analisador sintático. LEXEMA É uma ocorrência de um token no texto fonte. Durante o processamento do programa fonte, o analisador léxico faz a leitura caractere a caractere, da esquerda para a direita, linha a linha do código, buscando identificar os tokens e iniciando o preenchimento da tabela de símbolos. Durante este processamento, ele realiza várias tarefas: Extração e classificação dos identificadores. Eliminação de delimitadores e comentários. Identificação de elementos da linguagem, como: palavras reservadas, operadores, etc. Recuperação de erros. Internamente, os lexemas podem ser representados de duas formas básicas: Pelo próprio token, no caso de palavras reservadas e delimitadores. Por um par ordenado no qual o primeiro elemento identifica a classe do símbolo e o segundo um ponteiro para a área onde ele foi armazenado, como no caso de identificadores. Analisadores léxicos são, normalmente, implementados como autômatos finitos que trabalham como um reconhecedor de linguagens regulares. Quanto ao tratamento de erros, eles são relatados, mas a sua identificação não interrompe o processo de análise. A TAREFA DE CONSTRUIR UM ANALISADOR LÉXICO PODE SER INTEIRAMENTE AUTOMATIZADA, BASTANDO APENAS UMA ESPECIFICAÇÃO FORMAL DA LINGUEM A SER RECONHECIDA E UM SOFTWARE GERADOR DE ANALISADORES LÉXICOS. Considerando a nosso LPE, quais seriam os lexemas do programa abaixo: Programa exemplo: 1 { 2 Inteiro A, B, C; 3 B != 6; 4 C := 7; 5 A := B+C; 6 } Os lexemas seriam: PROGRAMA EXEMPLO { INTEIRO A , B , C ; B 6 ; C := 7 A := B + C ; } ATENÇÃO Destes EXEMPLO, A, B e C são identificadores e seriam acrescentados à tabela de símbolos. Seria ainda gerado um relatório de erros referente a linha 3 já que != não é um token válido na LPE. ANALISADOR SINTÁTICO O analisador sintático é o responsável pela execução do segundo passo da compilação. Ele agrupa os tokens gerados pelo analisador léxico em estruturas sintáticas (por exemplo, expressões, comandos) e as analisa verificando se atendem às regras de sintaxe da gramática da linguagem, gerando, se for o caso, um relatório com os erros encontrados. O analisador sintático produz (explícita ou implicitamente) uma árvore sintática. Em muitos compiladores, a árvore produzida não representa todo o código fonte, mas uma arvore compactada que visa a eliminar as redundâncias e demais elementos supérfluos. Os algoritmos de análise sintática podem ser de duas classes: ANÁLISE ASCENDENTE ANÁLISE DESCENDENTE ANÁLISE ASCENDENTE Quando a construção da árvore sintática é executada de baixo para cima, ou seja, das folhas para a raiz. Apesar de tratarem um maior número de gramáticas, são de difícil implementação e, geralmente, utilizados em geradores de analisadores sintáticos. ANÁLISE DESCENDENTE Quando a construção da árvore ocorre da raiz para as folhas. É o mais utilizado para a construção dos analisadores devido a sua maior facilidade de implementação. ATENÇÃO Geradores de analisadores sintáticos são programas que geram analisadores sintáticos a partir da especificação de uma gramática. A interação do analisador sintático com os demais componentes da etapa de análise está presente na Figura 25. Fonte: O autor, 2020. Figura 25 - Interação léxico/sintático/semântico. Voltando a nossa LPE, vamos considerar as seguintes definições de sintaxe em BNF, onde em negrito temos os símbolos da linguagem: <declaração de programa> ::= programa <identificador> {bloco_de_comando} <tipo> ::= inteiro | real | string <bloco_comando> ::= { [{comando}] } comando ::= bloco_comando | declaração_variavel | comando_se | atribuição declaração_variavel ::= <tipo> <identificador> [ {,<tipo> <identificador>} ]; comando_se ::= se (expressão) então <comando> [senão <comando>] ; <atribuição> ::= <identificador> <opeatv> <expressão> ; <Opeatb> ::= := <expressão> ::= <identificador> <oparit> <identificador> | <identificador> <oparit> <literal> | <identificador> <opcomp> <identificador> | <identificador> <opcomp> <literal> | <identificador> | <literal> <oparit> ::= + | - | * | / <opcomp> = | > | < | >= | <= | <> <literal> ::= inteiro | real | string <Identificador> ::= {L} + {L}|{D} <Inteiro> ::= {D}+ <Real> ::= {D} + [.{D}+] | .{D} <String> ::= ‘{C}’ L ::= A..Z a..z D ::= 0..9 A partir da definição desta sintaxe, analisemos o seguinte programa: 1 Programa exemplo 2 { 3 Inteiro A,B,C; 4 B := 6; 5 C := 7; 6 A := B+C; 7 } A primeira linha corresponde à declaração do programa e está correta. A segunda linha corresponde à abertura de um bloco de comando que se fecha na última linha, de forma que está correto. A terceira linha corresponde à declaração de variáveis e está de acordo com a gramática. As linhas 4, 5 e 6 correspondem a atribuições, sendo que a linha 6 inclui um operador aritmético e estão corretas. Atenção! Para visualização completa da tabela utilize a rolagem horizontal Desta forma, o programa está sintaticamente correto e geraria a árvore compactada da Figura 26. Fonte: O autor, 2020. Figura 26 - Árvore sintática. Considere agora o seguinte comando: 1 Se (A > B ) 2 Então 3 A := A + 5; 4 B := B – 5; 5 Senão 6 { 7 A := A - 5; 8 B := B + 5; 9 } ELE ESTÁ CORRETO? VAMOS ANALISAR: RESPOSTA A cláusula Se está de acordo com a sintaxe. O Então possui dois comandos, mas o bloco não foi aberto, ao contrário do que aconteceu no Senão, que utilizou as chaves {}. Portanto, o programa possui um erro sintático, pois o analisador irá considerar que existe apenas um comando no Então e, ao encontrar o Senão, interpretará que existe um Senão sem o Se/Então, erro que será relatado pelo analisador. Fonte: Shutterstock. ANALISADOR SEMÂNTICO EM MUITOS COMPILADORES, O ANALISADOR SEMÂNTICO TRABALHA INTEGRADO AO ANALISADOR SINTÁTICO. SUA FINALIDADE É VERIFICAR SE AS ESTRUTURAS SINTÁTICAS FAZEM SENTIDO EM SEU CONTEXTO, POR EXEMPLO, VERIFICARSE UMA VARIÁVEL É UTILIZADA COMO TAL, BEM COMO FAZER A VERIFICAÇÃO DE TIPO. Estas análises são realizadas a partir da árvore sintática gerada no passo anterior e das informações existentes na tabela de símbolos e produz como resultado uma representação intermediária do programa. Dos passos da análise, este é o mais difícil, pois envolve interpretar o significado das construções sintáticas do programa fonte. Vejamos um exemplo a partir do seguinte programa: 1 Programa exemplo 2 { 3 Inteiro A, C; 4 String B 5 B := ‘C’; 6 A := B; 7 } Note que o programa está do ponto de vista léxico e sintático correto. Porém, do ponto de vista semântico, possui um erro, já que a variável A foi definida como sendo do tipo INTEIRO e, portanto, não pode sofrer a atribuição de uma STRING. Este erro então deverá ser relatado pelo analisador. ETAPA DE SÍNTESE Apesar de, academicamente, consideramos que a etapa de síntese engloba 3 passos (geração de código intermediário, otimização e geração de código final), nos casos reais, eles atuam em conjunto, interagindo entre si, recursivamente, e tendo como grande produto, o código de montagem. Dessa forma, vamos abordá-los como um todo. RELEMBRANDO Devemos lembrar que o código de montagem é totalmente dependente da arquitetura da máquina, onde irá rodar e, para cada código fonte, existem infinitos programas objetos equivalentes, ou seja, produzem a mesma saída a partir da mesma entrada. A representação intermediária recebida da etapa de análise apresenta as instruções na ordem em que foram encontradas. Dessa forma, ela apresenta um programa com estratégias de implementação gerais que funcionarão em qualquer contexto. Porém, como o programa será “compilado” para uma determinada arquitetura, o código será executado em um contexto mais restrito e previsível e o gerador de código irá reescrever o programa para uma execução mais eficiente em um ambiente específico. Após receber o produto da etapa de análise, o compilador começa a realizar transformações na representação e otimizações. Na busca da otimização, o compilador busca detectar padrões que possam ser substituídos por construções equivalentes, mas que sejam mais eficientes. Esses padrões podem ser globais ou locais e a substituição pode ser dependente ou independente da máquina. Fonte: Shutterstock. Esta otimização pode gerar um grande custo computacional, portanto, nas implementações reais, dificilmente, será produzido o melhor programa objeto possível, mas o melhor dentro de um custo aceitável. Dessa forma, a otimização teria uma descrição mais exata como melhoria de código. AS OTIMIZAÇÕES, NORMALMENTE, CONSISTEM EM UMA ANÁLISE E UMA TRANSFORMAÇÃO, SENDO QUE A ANÁLISE VISA DETERMINAR ONDE O COMPILADOR PODE APLICAR A TÉCNICA COM SEGURANÇA E QUE GERA UM GANHO NO DESEMPENHO. Vários tipos de análise podem ser utilizados para suportar as transformações, por exemplo, análise de fluxo de dados ou análise de dependências. Já quanto às transformações, elas podem ser de vários tipos, por exemplo, encontrar cálculos que não mudam, que estejam em um laço e movê-los para locais executados com menor frequência. No passo de geração do código de montagem, ocorre a seleção de instruções, que consiste em mapear as operações da IR (representação intermediária), após a otimização, e reescrevê-las para comandos assembly equivalentes aos comandos de máquina do processador alvo. Por exemplo, o código otimizado da Figura 27 A poderia ser reescrito no código de montagem da Figura 27 B. Fonte: O autor. Figuras 27 A e B - Seleção de instruções. Durante esta primeira reescrita, o compilador ignora totalmente as limitações do hardware quanto à quantidade de registradores, ele considera que existem infinitos registradores virtuais. Ocorre então, uma segunda reescrita denominada alocação de registradores, onde os registradores virtuais são mapeados para os registradores reais. Durante este processo, o gerador de código determinará quais valores permanecem nos registradores reais e quais serão deslocados para a memória. Finalmente, uma terceira fase de reescrita ocorre correspondendo ao escalonamento de instruções quando as operações são reordenadas para melhor lidar com as restrições de desempenho da máquina alvo. Este escalonamento decorre da diferença de velocidade de execução de algumas instruções e da dependência de determinadas operações do resultado de outras operações. GERADORES DE COMPILADORES ATENÇÃO Escrever um compilador pode ser uma tarefe hercúlea. Para facilitar e dar suporte ao desenvolvimento dessas ferramentas, foram desenvolvidos programas que auxiliam a sua construção. Estes programas variam desde simples reconhecedores de linguagens regulares até complexos otimizadores de código objeto. Exemplos desses produtos são: GALS Possui uma interface gráfica e permite a geração automática de analisadores Léxico e Sintático a partir da definição de seus tokens e de sua gramática. Possui recursos didáticos que permitem a visualização das tabelas de símbolos. Possui ainda um simulador léxico e um simulador sintático. LEX Projetado para o processamento léxico de caracteres. Trabalha a partir de definição de uma linguagem regular. Produz um programa em C que permite realizar a análise léxica a partir da gramática especificada como entrada. YACC Utilizado, normalmente junto com o LEX, foi projetado para reconhecer linguagens recursivamente enumeráveis, trabalhando desta forma com linguagens de programação. Realiza a geração de um analisador sintático na linguagem C a partir de uma especificação de entrada. JavaCC Gera um analisador sintático em Java a partir de uma gramática especificada como entrada. Gera, de forma subjacente, um analisador léxico também. Utilizar uma abordagem top down. Atenção! Para visualização completa da tabela utilize a rolagem horizontal CONSTRUINDO UM PROGRAMA VERIFICANDO O APRENDIZADO 1. A PRIMEIRA ETAPA DA ANÁLISE DA COMPILAÇÃO É REALIZADA PELO ANALISADOR LÉXICO E SERVE COMO INTERMEDIÁRIO ENTRE O PROGRAMA FONTE E O ANALISADOR SINTÁTICO. DURANTE A ANÁLISE LÉXICA, A OCORRÊNCIA DE UM SÍMBOLO DA LINGUAGEM NO PROGRAMA FONTE É DENOMINADA: A) Token. B) Lexema. C) Sentença. D) Estrutura léxica. 2. DURANTE A FASE FINAL DA SÍNTESE, O CÓDIGO PASSA POR VÁRIAS REESCRITAS. O MAPEAMENTO DAS INSTRUÇÕES DO CÓDIGO OTIMIZADO PARA AS INSTRUÇÕES ESPECÍFICAS DA MÁQUINA DENOMINA-SE: A) Escalonamento de instruções. B) Geração de código binário. C) Seleção de instruções. D) Montagem. GABARITO 1. A primeira etapa da análise da compilação é realizada pelo analisador léxico e serve como intermediário entre o programa fonte e o analisador sintático. Durante a análise léxica, a ocorrência de um símbolo da linguagem no programa fonte é denominada: A alternativa "B " está correta. Os símbolos da linguagem constituem os seus tokens. Durante a fase de análise léxica, o compilador irá procurar as suas ocorrências no código fonte para, posteriormente, passá-las ao analisador sintático. Estas ocorrências são denominadas lexemas. 2. Durante a fase final da síntese, o código passa por várias reescritas. O mapeamento das instruções do código otimizado para as instruções específicas da máquina denomina-se: A alternativa "C " está correta. A seleção de instruções corresponde ao mapeamento das operações em representação intermediária que o gerador de código Intermediário e o otimizador geraram para as instruções em linguagem de baixo nível (assembly) da arquitetura da máquina para a qual o programa está sendo traduzido. CONCLUSÃO CONSIDERAÇÕES FINAIS Ao longo deste tema, fizemos uma viagem pelos conceitos relacionados ao processo de tradução, onde pudemos conhecer os vários tipos de tradutores de programas fonte para linguagem de máquina. Iniciamos estudando o que é uma linguagem de máquina e como ela funciona, a seguir, conhecemos o montador, um tipo de tradutor de programas em linguagem de baixo nívelpara a linguagem de máquina. Nossa próxima parada foi nas linguagens de alto nível, vendo suas características, e os vários tipos de tradutores para esta linguagem como interpretadores, compiladores e tradutores híbridos. A seguir, tratamos especificamente do compilador e vimos as etapas de análise e síntese, descrevendo os passos que compõem cada uma das etapas. Finalmente, em nossa parada final, estudamos com mais detalhes os vários componentes do compilador, como os analisadores léxico, sintático e semântico e os componentes do Back End. AVALIAÇÃO DO TEMA: REFERÊNCIAS AHO, A. V.; LAM, M. S.; JEFFREY, R. S. Compiladores: princípios, técnicas e ferramentas. 2 ed. São Paulo: Pearson, 2008. LOUDEN, K. C. Compiladores: princípios e práticas. São Paulo: Cengage Learning, 2004. SANTOS, P. R.; LANGLOIS, T. Compiladores: da teoria à prática. Rio de Janeiro: LTC, 2018. SEBESTA, R. W. Conceitos de linguagens de programação. 11. ed. Porto Alegre: Bookman, 2018. TANENBAUM, A. S. Organização Estruturada de Computadores. 5 ed. São Paulo: Pearson, 2006. EXPLORE+ Para saber mais sobre os assuntos explorados neste tema: Acesse a página do projeto GALS e se familiarize com este gerador de compiladores. CONTEUDISTA SIDNEY NICOLAU VENTURI FILHO HTTP://LATTES.CNPQ.BR/1495541534303386 javascript:void(0);
Compartilhar