C.2 Template para execução do Script de Recompilações
1.1 Linguagens compiladas e linguagem interpretadas
1.1.1 Compilação
No processo de compilação a linguagem de programação contida no código fonte deve ser transformada em uma linguagem de máquina, a qual será posteriormente execu- tada pelo processador (FISCHER; GRODZINSKY, 1993).
O processo de compilação requer a utilização de um compilador, o qual consiste em um programa ou conjunto de programas que tem como entrada o código fonte es- crito em uma linguagem de programação e como saída, cria um programa sematicamente equivalente uma outra linguagem, chamado de código objeto (COOPER, 2003, pág. 2).
Após a compilação, a execução do código objeto criado a partir do código fonte de entrada deve ser requisitado pelo usuário para que o processador execute as instruções de máquina (SCOTT, 2009, pág. 17).
As linguagens que, em geral, são compiladas, tem melhor performance em relação as linguagens que, em geral, são interpretadas. Isso porque as decisões que são tomadas em tempo de compilação não são necessárias no tempo de execução (AHO, 2008, pág. 2). Por exemplo, se um compilador garantir que em uma variável x seja sempre alocada em uma mesma posição na memória, ele pode gerar instruções de máquina que acesse esta localização sempre que se referenciar a variável x. Por outro lado, um interpretador precisa procurar por x em uma tabela a cada acesso, a fim de determinar sua localização (FISCHER; GRODZINSKY,1993, pág. 167).
Como exemplos de linguagens que, em geral, são compilada, temos Ada, ALGOL, BASIC, C, C++, COBOL, Cobra, Common Lisp, D, Delphi, Eiffel, Fortran, Objective-C, Pascal, Visual Basic, Visual Prolog.
1.1.2
Interpretação
No processo de interpretação, o código fonte é passado para um programa chamado interpretador, o qual é responsável pela leitura do código e pela tradução, em tempo real, deste código em instruções a serem executadas pelo sistema operacional ou pelo processador (FISCHER; GRODZINSKY,1993). No caso de linguagens que são, em geral, interpretadas, na maioria dos casos são nomeadas como scripts, embora esta não seja uma regra geral.
Diferente do compilador, o interpretador faz a leitura do código fonte e executa as respectivas instruções em tempo real. Isso faz com que códigos interpretados tenham maior flexibilidade e um melhor diagnóstico (exibição de mensagens de erros) do que códigos compilados. A interpretação também pode lidar melhor com linguagens de programação nas quais características fundamentais, tais como o tamanho e o tipo das variáveis, podem depender dos dados de entrada (SCOTT, 2009, pág. 17).
Mesmo que uma linguagem seja projetada inicialmente para ser compilada é possí- vel que exista interpretadores para estas linguagens, como é o caso do CINT1e CLING2.Estes
interpretadores podem ajudar no processo de desenvolvimento como descrito acima, no entanto existem limitações, como no caso do CINT3:
• não é possível definir um novo tipo de dado baseado em estrutura(struct) dentro de uma função;
• não suporta o tipo unsigned long long, interpretando-o como long long;
• o próprio CINT contém a definição do tipo bool, e carrega o mesmo na execução do programa;
1
Interpretador de código C/C++ encontrado em <https://root.cern.ch/cint>. 2
Interpretador de código C/C++ com back-end clang encontrado em<https://root.cern.ch/cling>. 3
• na declaração de ponteiro é necessário um espaço em branco antes e depois do ’*’; • não aceita o operador de sequenciamento ‘,’;
• um dos maiores problemas do CINT é a definição de macros: ele suporta apenas macros simples.
Sem o recurso da interpretação, seria uma tarefa deveras complexa implementar alguns recursos presentes em linguagens Lisp e Prolog onde, por exemplo, um programa pode escrever novas peças de si mesmo e executá-las em tempo real (SCOTT, 2009, pág. 17). Como exemplos de linguagens que, em geral, são interpretadas as linguagens: ActionScript, APL, ASP, BASIC, C#, CYBOL, Java, JavaScript, Lisp, Logo, Lua, PHP, Python, Ruby, Scheme, Smalltalk, VBScript.
1.1.3
Máquinas Virtuais
Para algumas linguagens existe o caso em que o compilador tem o papel de con- verter o código fonte em um byte code (SEBESTA, 2010, pág. 49). O byte code é uma linguagem de baixo nível similar a linguagem de máquina, que deve ser interpretada por um outro programa chamado Maquina Virtual (Virtual Machine – VM ).
Esta estratégia mista de pré-compilar o código para uma linguagem intermediária e interpretá-la em uma máquina virtual é denominada estratégia híbrida.
Como exemplo de linguagens que utilizam, em geral, a estratégia híbrida, temos Java, Python, JRuby, Oxygene, Rhino, Nashorn, JGNAT, Jython, Rakudo Perl 6.
1.1.4
Just In Time
O termo Just In Time – JIT – veio de um novo modelo de negócio da indústria manufatureira: uma estratégia onde a produção ou compra era feita sobre demanda, ao invés da utilização de estoques (CARDOSO; AULER, 2014, pág. 177). Este modelo tem como vantagens menores custos com armazenamento, menos desperdícios, resposta mais rápida aos clientes e maior produção potencial.
Na compilação, esta analogia se adapta bem por que um compilador JIT não armazena os binários do programa no disco (o estoque), mas começa a compilação apenas de partes do programa necessárias durante a execução (FISCHER; GRODZINSKY,1993, pág. 8).
Foram desenvolvidas, principalmente para linguagens que são, em geral, interpre- tadas, ferramentas que se valem do JIT para acelerar a interpretação e execução dos códigos-fonte (scripts) como, por exemplo, o PyPy para a linguagem Python.4
4
1.2 Processo de Compilação
1.2.1
Preprocessadores
Antes de um código-fonte passar pelo processo de compilação, pode ser necessária a execução de um programa, denominado preprocessador, que tem como responsabilidade preparar o código-fonte para a compilação.
Dentre as possíveis tarefas e características comuns a um preprocessador, podemos citar:
• Processamento de macros: um preprocessador pode permitir que um usuário defina macros que sejam abreviações para construções mais longas (AHO, 2008, pág. 8); • Inclusão de arquivos: um preprocessador pode incluir arquivos cabeçalho no texto
do programa. Por exemplo, o preprocessador faz com que o conteúdo de um arquivo externo seja transcrito no código-fonte no ponto onde existe a marcação para sua inclusão (FISCHER; GRODZINSKY, 1993, pág. 8);
• Preprocessadores “racionais” : este tipo de preprocessador é responsável por permitir a construção de macros utilizando condicionais while ou if mesmo em linguagens que não suportem tais estruturas (FISCHER; GRODZINSKY, 1993, pág. 8); • Extensões de linguagens: são formas de conferir maior poder as linguagens através
de macros embutidas. Por exemplo, Equel5 é uma linguagem de interrogação embu-
tida em C que permite a manipulação de banco de dados. Os enunciados começados com ## são considerados pelo preprocessador como comandos de acesso ao banco de dados, os quais não fazem parte da linguagem C e, quando traduzidos, são con- vertidos em rotinas que tratam este acesso ao banco de dados (AHO, 2008, pág. 8).
1.2.2
Compilação
No processo de compilação tradicional, o compilador atua em duas fases principais: análise e síntese (SCOTT,2009, pág. 26). A Figura 1ilustra estas fases.
A fase de análise (ou front-end) de um compilador é a fase que tem como objetivo entender o código fonte e representá-lo em uma estrutura intermediária que facilite sua manipulação posterior. Esta fase é subdividida em análise léxica, análise sintática, análise semântica e geração de código intermediário (SCOTT, 2009, pág. 36).
A fase de síntese (ou back-end) de um compilador é a fase que tem como objetivo realizar a geração de código final, otimizando o código analisado na fase de sintase e
5
Figura 1: Fases da Compilação (SCOTT, 2009, pág. 26), com adaptações.
gerando um código sematicamente igual ao código fonte e com melhorias de performance e espaço. Esta fase é subdividida em otimização de código independente do alvo, geração de código alvo e otimização de código para o alvo específico (SCOTT,2009, pág. 36).
1.2.3
Análise Léxica
A análise léxica é a primeira fase a ser executada pelo compilador (AHO; ULLMAN,
1972, pág. 59). A função do analisador léxico, também denominado scanner, é ler o có- digo fonte, caractere a caractere, buscando a separação e a identificação dos elementos do programa, denominados símbolos léxicos ou tokens (PRICE; TOSCANO, 2000, pág. 195). Assim, é produzida uma sequência de tokens que será utilizada na análise sintática (AHO, 2008, pág. 38).
Esta fase também tem a importância de realizar a remoção de elementos “deco- rativos” do programa, tais como espaços, tabulações, caracteres de avanço e comentários (AHO; ULLMAN,1972, pág. 59). Para auxiliar a construção deste analisador, estão dispo- níveis uma série de geradores automáticos de analisadores léxicos, cujo objetivo é reduzir o esforço de programação deste tipo de ferramenta, especificando-se apenas os tokens a serem reconhecidos (FISCHER; LEBLANC, 1991, pág. 50).
1.2.4
Análise Sintática
A análise sintática, ou análise gramatical, é o processo de determinar se uma cadeia de símbolos léxicos pode ser gerada por uma gramática pré-definida (AHO, 1986).
O analisador sintático é o responsável por verificar se os símbolos contidos no código fonte formam um programa válido ou não (DELAMARO, 2004, pág. 38).
A maioria dos métodos de análise sintática são de dois tipos, denominados top-
downou bottom-up (LEWIS; ROSENKRANTZ,1978, pág. 227). Como indicado por seus nomes, os analisadores sintáticos top-down constroem árvores do topo para as folhas, enquanto o analisadores bottom-up começam das folhas e constroem a árvore de baixo para cima até chegar na raiz. Em ambos os casos são percorridas da esquerda para a direita, símbolo a símbolo. Estes dois tipos são utilizados devido seu potencial expres- sivo para descrever a maioria das construções sintáticas das linguagens de programação (DELAMARO,2004, pág. 38). Para auxiliar na criação de analisadores sintáticos existem disponíveis uma série de geradores automáticos, como por exemplo, o Flex6, o Bison7 e o
JavaCC8 (ALBLAS; NYMEYER, 1996, pág. 30).
1.2.5
Análise Semântica
O analisador semântico tem como função prover métodos para que as estrutu- ras construídas pelo analisador sintático possam ser avaliadas ou executadas (WATSON,
1989, pág. 337). Estas validações são feitas para assegurar que certos tipos de erros de programação sejam detectados e reportados. Os exemplos de verificação incluem decla- ração de tipo, declaração de funções, sobrecarga de funções, sobrecarga de operadores, verificação de fluxo de controle, verificação de operações lógicas e aritméticas válidas entre variáveis e a verificação de unicidade de variáveis em determinados escopos da linguagem (SCOTT, 2009, pág. 147).