6

Configurando o Setup

Introdução6.1

Para construirmos um compilador, precisamos primeiro estabelecer uma base sólida e confiável. Escolhemos a linguagem C para essa jornada por ser frequentemente considerada a "língua mãe" da infraestrutura de sistemas. Ela nos oferece o controle de baixo nível necessário para manipular bytes, acessar a memória diretamente e gerenciar arquivos, tudo isso sem perder a elegância e a organização de uma linguagem estruturada.

Se você sente que precisa reforçar seus conhecimentos ou deseja aprender a linguagem do zero antes de avançarmos, recomendamos fortemente que consulte o material da disciplina TEP II. Uma revisão cuidadosa sobre ponteiros e alocação dinâmica de memória será especialmente útil, visto que lidaremos constantemente com estruturas de dados complexas para representar o código fonte em memória.

Nesta etapa inicial, nosso foco será construir o alicerce do projeto. Antes de escrevermos a primeira linha do analisador léxico, que é o componente responsável por ler o texto e identificar os tokens, precisamos configurar nosso ambiente de programação e definir como esse código será compilado, organizado e executado.

Máquina Virtual6.2

No Ambiente Virtual de Aprendizagem (AVA) da disciplina, você deve baixar o arquivo da máquina virtual Linux fornecido. Caso esteja utilizando os computadores do laboratório da Faesa, verifique se já existe um arquivo ou máquina virtual identificada como Alpine - Faesa.

Arquivo da máquina virtual Alpine Faesa

É importante que você note que trabalharemos com o conceito de duas máquinas virtuais distintas ao longo da disciplina. A primeira é a máquina virtual de desenvolvimento que estamos configurando agora, que é o sistema operacional onde você escreverá e compilará seu código. A segunda será a máquina virtual alvo, que desenvolveremos como parte do projeto para executar o código compilado pela nossa linguagem. Essa segunda máquina funcionará de maneira análoga à JVM (Java Virtual Machine), processando a linguagem intermediária que geraremos.

Ao abrir o Oracle VirtualBox Manager, você deve importar o arquivo fornecido. Vá em File $\rightarrow$ Import Appliance e, clicando no ícone de pasta com uma seta verde, selecione o arquivo Alpine - Faesa.ova. Clique em Finish para concluir a importação. Com isso, uma nova máquina deverá aparecer no menu lateral. Dê dois cliques sobre ela para iniciá-la.

A máquina inicializará em uma tela preta de terminal. Não se assuste com essa interface minimalista, pois isso foi feito propositalmente para manter o arquivo leve e compacto, permitindo que você o baixe e utilize em casa sem grandes dificuldades. A tela inicial deve se parecer com a imagem abaixo:

Tela de login do Alpine Linux

Normalmente, a janela abre com um tamanho reduzido que pode esconder a última linha de entrada. Para corrigir isso, vá em View na barra superior e altere para Scaled Mode. Agora você deve ser capaz de visualizar o prompt alpine login:, que aguarda o seu nome de usuário. Digite root e pressione Enter. Em seguida, o sistema pedirá a senha. Digite 123 e pressione Enter novamente. Note que, por medidas de segurança, os caracteres da senha não aparecem na tela enquanto você digita, mas o sistema está capturando sua entrada. Após esse processo, você verá que o login foi realizado com sucesso.

Apesar de ser possível escrever código diretamente nesse terminal, essa não é a forma mais produtiva de trabalhar. Por isso, configuraremos o Visual Studio Code para utilizar o ambiente da máquina virtual remotamente, permitindo que você escreva o código em uma interface gráfica moderna e confortável. No terminal da máquina virtual, digite o comando ip a e pressione Enter. Você verá informações de rede semelhantes às da imagem a seguir.

Comando ip a mostrando o endereço de rede

Observe o endereço IP destacado na saída do comando e anote-o. Agora, abra o Visual Studio Code e instale a extensão chamada Remote - SSH, desenvolvida pela Microsoft, caso ela ainda não esteja instalada. Após a instalação, um ícone de monitor com duas setas deve aparecer na barra lateral esquerda do VS Code, conforme mostrado abaixo:

Ícone Remote SSH no VS Code

Ao colocar o mouse sobre o título SSH na barra lateral, aparecerão ícones de engrenagem e de adição (+). Clique no ícone de adição (+) e uma caixa de texto surgirá no topo da janela. Digite root@IP, substituindo IP pelo número que você anotou do terminal (por exemplo, [email protected]), e pressione Enter.

Adicionando conexão SSH no VS Code

Caso apareça um menu pedindo para selecionar um arquivo de configuração SSH, selecione a primeira opção sugerida. Após isso, o endereço da máquina adicionada aparecerá na lista abaixo do título SSH. Passe o mouse sobre esse item e clique na seta que surge para conectar. O VS Code solicitará a senha; digite 123 (a mesma usada para o login). Se um menu aparecer pedindo para confirmar o "fingerprint" da conexão, selecione "Linux" se perguntado sobre o sistema operacional e depois escolha "Continue" ou digite "yes".

Pronto! Se você observar o canto inferior esquerdo do VS Code e vir o número IP da conexão, significa que você está dentro da máquina virtual. Agora você pode usar o editor para criar pastas e programar diretamente no ambiente Linux. Clique em Open Folder na barra lateral e pressione Enter, certificando-se de que o caminho no campo de entrada seja apenas /root/.

Agora você pode criar sua própria pasta de trabalho, por exemplo, com o nome compilador. Caso queira salvar seu progresso ao final da aula, você poderá baixar essa pasta inteira clicando com o botão direito sobre ela e selecionando Download....

Opção de download de arquivos no VS Code

Sempre baixe os arquivos!

Lembre-se de que os arquivos podem ser perdidos se os computadores do laboratório forem reiniciados ou formatados. Caso não esteja utilizando seu computador pessoal, crie o hábito de sempre baixar a pasta do projeto ao final da aula e salvá-la em um local seguro, como um pendrive ou na nuvem.

Estruturando o Projeto6.3

Antes de configurarmos o sistema de compilação, é essencial organizarmos a estrutura de diretórios e arquivos do nosso projeto. Um compilador é um software complexo e, sem uma organização rigorosa, o código pode se tornar incontrolável rapidamente.

Dentro da pasta compilador que você acabou de criar, crie a seguinte estrutura de subpastas:

  1. Uma pasta chamada src (source), onde ficará todo o código fonte .c.
  2. Dentro de src, crie uma subpasta chamada compiler.
  3. Na raiz do projeto (ao lado de src), crie uma pasta chamada include, onde ficarão os arquivos de cabeçalho .h.

O Espelhamento de Cabeçalhos6.3.1

A linguagem C possui uma característica arquitetural distinta, a separação clara entre as definições (interfaces) e as implementações (código lógico). Enquanto os arquivos .c contêm a lógica de como as funções operam, os arquivos .h (headers) servem como contratos, informando ao restante do programa quais funções estão disponíveis e como utilizá-las.

Para manter nosso projeto organizado e profissional, adotaremos a prática de espelhamento de diretórios. Isso significa que a estrutura interna da pasta include deve ser idêntica à estrutura da pasta src. Dessa forma, para cada arquivo de implementação src/compiler/lexer.c, existirá um arquivo de definição correspondente em include/compiler/lexer.h. Isso facilita imensamente a navegação pelo projeto conforme ele cresce.

Vamos aplicar essa estrutura agora:

  1. Dentro de include, crie também uma subpasta chamada compiler.
  2. Dentro de include/compiler/, crie os arquivos vazios token.h e lexer.h.

Criando os Arquivos de Implementação6.3.2

Com os cabeçalhos definidos na estrutura, vamos criar os arquivos de implementação vazios para que o CMake possa mapear o projeto completo:

  • Dentro de src/compiler/, crie os arquivos token.c e lexer.c.
  • Dentro de src/, crie o arquivo main.c.

Ao final deste processo, sua árvore de arquivos deve estar organizada exatamente como mostrada abaixo:

compilador/
├── include/
│   └── compiler/
│       ├── lexer.h
│       └── token.h
├── src/
│   ├── compiler/
│   │   ├── lexer.c
│   │   └── token.c
│   └── main.c
└── (aqui ficará o CMakeLists.txt)

Com esses arquivos criados, garantimos que o sistema de build conseguirá encontrar tanto as definições (.h) quanto as implementações (.c), permitindo uma compilação modular e organizada.

CMake6.4

Muitas vezes, ao iniciar um projeto simples em C, recorremos ao comando manual do compilador (gcc main.c -o compiler) ou a arquivos Makefile artesanais. Entretanto, conforme o nosso compilador cresce, gerenciar dezenas de arquivos fonte, cabeçalhos, dependências e bibliotecas torna-se uma tarefa hercúlea e propensa a erros humanos.

O CMake surge não como um compilador, mas como um gerador de sistemas de compilação. Ele atua como um maestro que lê um arquivo de configuração de alto nível (CMakeLists.txt) e decide a melhor forma de compilar seu projeto no sistema atual, seja criando um Makefile no Linux ou um projeto do Visual Studio no Windows. Isso nos permite focar na lógica do compilador, enquanto o CMake cuida da burocracia técnica da construção do binário.

Na raiz do seu projeto (fora das pastas src e include), crie um arquivo chamado CMakeLists.txt e insira o conteúdo abaixo. Observe a estrutura lógica que reflete a organização que acabamos de criar. O objetivo é manter a modularidade: o analisador léxico e o gerenciador de tokens ficarão encapsulados em uma biblioteca separada, enquanto o arquivo principal apenas coordena a execução.

cmake_minimum_required(VERSION 3.20)
project(compiler C)

set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_library(compiler_lib
    src/compiler/token.c
    src/compiler/lexer.c
)

target_include_directories(compiler_lib
    PUBLIC
        ${PROJECT_SOURCE_DIR}/include
)

target_compile_options(compiler_lib PRIVATE
    -Wall -Wextra -Wpedantic
)

add_executable(compiler
    src/main.c
)

target_link_libraries(compiler PRIVATE compiler_lib)

Vamos agora entender, de forma breve, o que cada bloco de comandos significa para que você tenha autonomia para alterar esse arquivo conforme o projeto evoluir.

Modularização e Bibliotecas6.4.1

A parte crucial para a arquitetura do nosso software está na definição da biblioteca compiler_lib. Ao utilizarmos o comando add_library, estamos dizendo ao CMake para agrupar os arquivos src/compiler/token.c e src/compiler/lexer.c em uma unidade lógica reutilizável.

Essa separação é vital por dois motivos. Primeiro, ela isola a complexidade: o analisador léxico (lexer) e a definição de tokens são componentes fundamentais que serão usados por várias partes do sistema, como o futuro analisador sintático. Segundo, ao transformar esses módulos em uma biblioteca, facilitamos a criação de testes unitários específicos para eles, sem precisar rodar o programa inteiro. O comando target_include_directories complementa essa configuração, informando ao compilador que qualquer arquivo que utilize essa biblioteca pode buscar seus cabeçalhos na pasta include, mantendo nosso código limpo e organizado.

O Binário e o Ponto de Entrada6.4.2

Por fim, definimos o produto final do nosso projeto através do comando add_executable(compiler src/main.c). Este comando instrui o CMake a gerar um arquivo executável chamado compiler utilizando o código presente em src/main.c.

O arquivo main.c será o ponto de entrada da nossa aplicação. É nele que reside a função main, responsável por receber os argumentos da linha de comando (como o nome do arquivo fonte que queremos compilar), inicializar os componentes do compilador e orquestrar o processo de leitura e análise.

Para garantir que o executável tenha acesso às funções do analisador léxico que definimos anteriormente, utilizamos o comando:

target_link_libraries(compiler PRIVATE compiler_lib)

Essa linha realiza a "linkagem" final, conectando o executável compiler à nossa biblioteca de ferramentas compiler_lib. A partir desse momento, o CMake entende que o executável depende da biblioteca; se você alterar qualquer linha de código no lexer, o sistema saberá automaticamente que precisa recompilar a biblioteca e atualizar o executável final.

Entre na pasta correta!

Antes de prosseguir, entre na pasta criada. Vá em "File" -> "Open Folder" e digite o caminho para a pasta criada, no caso desse tutorial /root/compilador/.

Até o momento sua pasta compilador deve estar semelhante à imagem abaixo:

Estrutura da pasta completa.

Como Executar6.4.3

Com o arquivo CMakeLists.txt configurado e a estrutura de pastas criada, o fluxo de trabalho para construir o projeto será sempre dividido em dois passos simples no seu terminal:

  1. Configuração: Digite cmake -B build e pressione Enter. Esse comando lê o seu CMakeLists.txt e cria uma nova pasta chamada build, contendo todas as instruções de compilação (Makefiles) necessárias.

  2. Construção: Digite cmake --build build e pressione Enter. Esse comando executa a compilação de fato, lendo as instruções da pasta build e gerando o arquivo executável compiler.

Essa separação é extremamente benéfica pois mantém a raiz do seu projeto limpa. Todos os arquivos temporários, objetos de compilação e artefatos gerados ficam isolados dentro da pasta build, que pode ser apagada e recriada a qualquer momento sem risco de perder seu código fonte.

Para começarmos com o código do compilador propriamente dito, teste o comando do cmake antes. Entretanto, se não definirmos a função de entrada main o processo de compilação dará erro. Portanto, no arquivo main.c cole o seguinte código:

#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

Após isso, no VS Code, utilize o atalho CTRL + J para abrir o terminal, aqui você poderá utilizar o terminal do linux da sua máquina virtual. Rode os dois comandos do cmake fornecidos acima. Você deve obter algo como na imagem:

Demonstração de uma mensagem de sucesso de build.

Note que dentro da pasta build existe um arquivo chamado compiler, ele é o nosso binário final (o resto dos arquivos são apenas auxiliares no processo de build). Para executá-lo, escreva no terminal ./build/compiler e veja na saída padrão a mensagem que digitamos no printf. Sempre que fizer uma alteração, lembre de compilar novamente seu código e executar o binário para testar.

Próximos Passos6.5

Agora que temos nossa estrutura de construção pronta, o CMake configurado e o ambiente virtual operante, estamos prontos para mergulhar na primeira etapa real do desenvolvimento. No próximo capítulo, Lab - Analisador Léxico I, estudaremos a teoria e a prática de como transformar um fluxo bruto de caracteres em unidades de significado que o computador consiga entender.