v1.2.0-STABLE
Este repositório se trata de um projeto da disciplina Laboratório de Compiladores da UNIFESP SJC no ano de 2020, onde a proposta foi desenvolver a um compilador para a linguagem C- proposta no livro Compiladores: Princípios e Práticas de Kenneth Louden. O compilador, além de fazer toda análise do código C-, gera um código binário baseado em um processador de 32 bits, feito anteriormente durante o curso, e arquivos de texto com todos os processos de compilação sendo eles a árvore sintática, a tabela de símbolos, o código intermediário e o código assembly.
Para informações mais detalhadas sobre a implementação do compilador confira o relatório presente na pasta Relatório.
O projeto do compilador foi baseado nas premissas de Kenneth Louden em Compiladores: Princípios e Práticas e foi majoritariamente feito na linguagem C, utilizando algumas ferramentas para auxiliar na criação de algumas partes.
A primeira ferramenta foi o compilador Flex, que foi utilizado para facilitar na criação de um Scaner sem ter a necessidade de programar linha por linha do mesmo, e sim inserir Expressões Regulares para a aceitação de cada token.
A segunda, foi o compilador YACC Bison, que foi utilizado em conjunto de uma Gramática Livre de Contexto para gerar um Parser. Essa ferramenta tem uma vantagem que é o fato de ser própria para a utilização em conjunto com o Flex, fazendo a criação de uma rotina de análise se tornar mais simples.
Ambas ferramentas depois de compiladas geram códigos C que podem ser facilmente integrados com as outas partes do projeto que foram desenvolvidas na mesma linguagem.
O projeto foi divido macroscopicamente em duas partes: a Análise e a Síntese, sendo cada uma delas subdivididas em mais 3 partes.
A fase de análise é a responsável por percorrer todo o código e verificar sua integridade e a correspondência com a linguagem proposta, no caso C-. Ela foi dividida em análise léxica¹ (scaner.l), análise sintática² (parser.y) e análise semântica³ (analyze.c e symtab.c). A função de 1 é percorrer todo o código fonte analisando se cada palavra escrita se encaixa no escopo da linguagem C-, gerando tokens que são passados para 2 que os verifica em conjunto e analisa se a estrutura do código está correta, gerando uma árvore sintática. Essa última por fim é repassada para 3 que procura por erros de contexto como variáveis não declaradas ou atribuições de tipos conflitantes e caso nenhum seja encontrado uma tabela pe gerada constando todos os itens presentes no código, bem como a posição de memória a ser alocada (respeitando as restrições do processador) e outras informações presentes na árvore sintática.
Exemplo de Tabela de Símbolos
Tabela de Símbolos:
---------------------------------------------------------------------------------
Nome Escopo Tipo ID Tipo Retorno Tipo Param Mem. Loc. Num da linha
------------- ------ ------- ------------ ---------- --------- ------------
main global fun VOID VOID - 10;
input main call VOID null - 13; 14;
return gcd ret INT null - 6;
return gcd ret INT null - 3;
output main call null null - 15;
u gcd var INT null 1 1; 3; 6; 6;
v gcd var INT null 2 1; 2; 6; 6; 6;
x main var INT null 1 11; 13; 15;
y main var INT null 2 12; 14; 15;
gcd gcd call INT null - 6; 15;
gcd global fun INT INT - 1;
A fase de síntese é executada apenas se a fase de análise foi executada completamente sem encontrar nenhum erro. Ela é responsável por gerar os códigos à partir do código fonte e é dividido em: Código Intermediário¹ (cgen.c), Código Assembly² (assembly.c) e Código Binário³ (binary.c). 1 é um código gerado pelo percorrimento da árvore sintática formado por quadruplas de três endereços, ou seja, todo o código é trazido para um nível mais baixo contendo uma label que referencia qual operação será feita e até três operandos informando onde o dado final será guardado e de onde os dados são retirados para que a operação possa ser feita. Em seguida cada quádrupla é convertida em um ou mais comandos assembly para gerar 2 e o mesmo pode ser convertido diretamente para 3 por estar no nível mais próximo possível de comandos binários para uma linguagem.
Além dos arquivos citados acima, também existem mais arquivos para que a conexão entre eles possa ser feita. Intuitivamente existem arquivos de cabeçalho .h que contém estruturas utilizadas em seus respectivos códigos, variáveis globais e funções que podem ser acessadas por outros arquivos. Também existem o cabeçalho globals.h que se trata do cabeçalho global com estruturas que e variáveis que são compartilhados por todo o projeto. Por fim existem os arquivos de utilidades, sendo eles o arquivo main.c que é o que contém a rotina de união do compilador e o arquivo util.c que se trata de rotinas úteis que podem ser usadas por todos os módulos.
Primeiramente, ao baixar o repositório, garanta que tenha instalado os pacotes do Flex e do Bison, em seguida abra o terminal na raiz da pasta do compilador e insira os seguintes comandos para conceder permissão aos scripts para a montagem e desmontagem do mesmo
$ chmod +x run.sh (montar o compilador)
$ chmod +x clean.sh (limpar arquivos de montagem)
Para executar o compilador, primeiramente é preciso montar o mesmo, compilando todos seus arquivos ao executar o primeiro script:
$ ./run.sh
Em seguida, mova o código C- para a pasta códigos e execute o código abaixo para compilar. Tenha certeza de que o arquivo contem a extensão .cm:
$ ./compilador [nome do arquivo a ser compilado]
Por fim, caso deseje apagar os arquivos gerados pela montagem do compilador, mantendo apenas seus códigos não compilados, execute o script abaixo:
$ ./clean.sh
Obs: Após a execução do script clean.sh o compilador não funcionará até o script run.sh seja executado novamente. Caso o script de limpeza não seja executado o compilador pode ser utilizado quantas vezes for desejado, mesmo após reiniciar o computador.
No arquivo main.c existe uma série de flags que controlam a geração de arquivos intermediários ou a impressão dos mesmos no terminal.
FlagType TraceScan = FALSE;
FlagType TraceParse = FALSE;
FlagType TraceAnalyze = FALSE;
FlagType TraceCode = FALSE;
FlagType PrintCode = FALSE;
FlagType CreateFiles = FALSE;
Respectivamente, as flags representam a impressão no terminal dos tokens léxicos, da árvore sintática, da tabela de símbolos, das labels de percorrimento durante a geração de código intermediário assim como a impressão dos códigos e da criação de arquivos com essas estruturas. O valor FALSE define a não geração/impressão da estrutura tratada pela flag, enquanto o valor TRUE define o caso contrário.
O compilador já está em sua fase final de testes (inclusive com testes integrados ao processador referente) e consegue fazer todo o processo de tradução de forma satisfatória. Porém, apesar já estar com sua funcionalidade completa, ainda existem alguns erros encontrados durante os testes que precisam ser refinados, sendo eles:
- Ao conter variáveis e chamadas de funções em uma atribuição a ordem dos loads podem impedir a recursão;
- Passagem de vetor local como parâmetro retorna erro semântico;
- Vetor passado como parâmetro duas vezes recebe o endereço errado.