Compiladores: Projeto Guiado Completo (Parte 2)
Guia completo para implementação das etapas finais do seu compilador: da análise semântica à geração de código otimizado, com exemplos práticos e código em Python.
Introdução ao Projeto Guiado (Parte 2)
Bem-vindos à continuação do nosso projeto guiado de compiladores! Nesta segunda parte, vamos finalizar a implementação do nosso compilador, partindo da análise semântica até a geração de código executável.
Na Parte 1, implementamos o analisador léxico (scanner) e o analisador sintático (parser), construindo a base do nosso compilador. Agora, vamos avançar para as etapas que transformam nossa árvore sintática em código funcional.
Pipeline completo de um compilador: da análise léxica à geração de código, passando pela análise semântica e otimizações.
Revisão da Parte 1 e Próximos Passos
O que já fizemos
  • Implementação do analisador léxico (lexer.py)
  • Criação do analisador sintático (parser.py)
  • Construção da AST (Árvore Sintática Abstrata)
  • Testes básicos de sintaxe da linguagem
O que faremos agora
  • Análise semântica com verificação de tipos
  • Implementação da tabela de símbolos
  • Geração de código intermediário
  • Otimizações básicas de código
  • Integração completa dos módulos
Nosso objetivo final é criar um protótipo funcional completo, capaz de traduzir um programa escrito em nossa linguagem para código executável, com tratamento adequado de erros em todas as etapas.
Fundamentos da Análise Semântica
A análise semântica verifica se um programa sintaticamente correto faz sentido conforme as regras da linguagem. É como verificar se uma frase gramaticalmente correta tem significado lógico.
Principais funções do analisador semântico:
  • Verificação de tipos e compatibilidade em expressões
  • Controle de escopo e visibilidade de variáveis
  • Verificação de declarações antes do uso
  • Validação de conversões implícitas de tipos
Durante a análise semântica, verificamos se as operações fazem sentido contextualmente, como multiplicar números, mas não textos com números.
A Tabela de Símbolos: Coração da Análise Semântica
O que é a Tabela de Símbolos?
Uma estrutura de dados que armazena informações sobre os identificadores (variáveis, funções, classes) declarados no programa. Para cada identificador, armazenamos seu tipo, escopo, valor (se constante) e outras informações relevantes.
Operações Principais
  • Inserir novos símbolos (declarações)
  • Buscar símbolos existentes (usos)
  • Verificar duplicidade de declarações
  • Gerenciar escopo (blocos aninhados)
Implementação em Python
Podemos usar dicionários aninhados ou classes específicas para representar a tabela. Para escopos aninhados, uma lista de dicionários ou uma árvore são estruturas adequadas.
A tabela de símbolos é consultada e atualizada durante toda a análise semântica e posteriormente usada na geração de código.
Implementação da Tabela de Símbolos
class SymbolTable: def __init__(self): # Pilha de escopos (o último é o escopo atual) self.scopes = [{}] def enter_scope(self): # Cria um novo escopo self.scopes.append({}) def exit_scope(self): # Remove o escopo atual if len(self.scopes) > 1: self.scopes.pop() def insert(self, name, type, line=None): # Insere símbolo no escopo atual if name in self.scopes[-1]: return False # Erro: redeclaração self.scopes[-1][name] = {'type': type, 'line': line} return True def lookup(self, name): # Busca do escopo atual para o global for scope in reversed(self.scopes): if name in scope: return scope[name] return None # Não encontrado
Esta implementação permite gerenciar escopos aninhados, essencial para linguagens com blocos como C, Java ou Python. Cada novo bloco (função, if, loop) gera um novo escopo.
Exemplo de Verificação Semântica
Código Fonte
int a = 5; float b = 3.14; string c = "texto"; a = b + c; // Erro: incompatibilidade de tipos
Este código contém um erro semântico: tentativa de somar um float (b) com uma string (c) e atribuir a um int (a). Sintaticamente está correto, mas semanticamente é inválido.
Verificação Semântica
  1. Declarações são inseridas na tabela de símbolos: a (int), b (float), c (string)
  1. Na expressão b + c, verificamos os tipos dos operandos
  1. Regra semântica: operador + requer tipos compatíveis
  1. float + string não é permitido → erro semântico
  1. Mesmo se fosse possível, atribuir para int exigiria conversão
A análise semântica detectaria este erro e impediria a geração de código inválido, fornecendo uma mensagem como: "Erro na linha 4: operador '+' não pode ser aplicado a float e string".
Integrando Análise Semântica ao Parser
A integração da análise semântica com o parser pode ser feita de duas formas principais:
Abordagem Integrada
Inserimos as verificações semânticas diretamente nas regras gramaticais do parser. Para cada regra sintática que requer verificação de tipos ou símbolos, adicionamos ações semânticas.
Abordagem Separada
Primeiro construímos a AST completa com o parser e depois percorremos essa árvore com um analisador semântico dedicado, realizando as verificações necessárias.
Para nosso projeto, usaremos a segunda abordagem, que mantém uma separação mais clara entre os módulos e facilita a manutenção.
Verificação de Tipos na AST
def semantic_analysis(ast, symbol_table): """Realiza análise semântica na AST""" if ast is None: return None # Verifica o tipo do nó atual if ast.node_type == 'BinaryOp': # Analisa recursivamente os operandos left_type = semantic_analysis(ast.left, symbol_table) right_type = semantic_analysis(ast.right, symbol_table) # Verifica compatibilidade de tipos if ast.op in ['+', '-', '*', '/']: if left_type == 'int' and right_type == 'int': return 'int' elif left_type in ['int', 'float'] and right_type in ['int', 'float']: return 'float' else: raise SemanticError(f"Operador '{ast.op}' incompatível com tipos {left_type} e {right_type}", ast.line) elif ast.node_type == 'VarDecl': # Registra variável na tabela de símbolos if not symbol_table.insert(ast.name, ast.type, ast.line): raise SemanticError(f"Variável '{ast.name}' já declarada", ast.line) return None # Outros tipos de nós...
Esta função percorre a AST recursivamente, verificando a semântica de cada nó e propagando informações de tipo para permitir a verificação de compatibilidade em expressões.
Erros Semânticos Comuns
1
Variável não declarada
Tentativa de usar uma variável que não foi previamente declarada no escopo atual ou em escopos visíveis.
x = y + 5; // Erro se 'y' não foi declarado
2
Incompatibilidade de tipos
Operações entre tipos incompatíveis ou atribuições inválidas.
int a = "texto"; // Erro: string não pode ser atribuída a int
3
Redeclaração de variável
Declarar a mesma variável duas vezes no mesmo escopo.
int x = 5; float x = 3.14; // Erro: 'x' já foi declarada
4
Uso incorreto de função
Chamar uma função com número ou tipo incorreto de argumentos.
int soma(int a, int b) { return a + b; } soma(1); // Erro: argumentos insuficientes
Tratamento de Erros Semânticos
Um bom compilador deve fornecer mensagens de erro claras e informativas para ajudar o programador a corrigir problemas em seu código. Para isso, implementaremos uma classe personalizada de erros semânticos:
class SemanticError(Exception): def __init__(self, message, line=None): self.message = message self.line = line super().__init__(self.format_message()) def format_message(self): if self.line: return f"Erro semântico na linha {self.line}: {self.message}" return f"Erro semântico: {self.message}"
A implementação deve capturar esses erros e apresentá-los de forma amigável:
def compile(source_code): try: # Análise léxica tokens = lexer.tokenize(source_code) # Análise sintática ast = parser.parse(tokens) # Análise semântica symbol_table = SymbolTable() semantic_analysis(ast, symbol_table) # Geração de código code = generate_code(ast, symbol_table) return code except SemanticError as e: print(e) return None
Geração de Código: Introdução
A geração de código é a etapa final do compilador, onde transformamos a AST verificada em código executável ou intermediário. Existem várias abordagens possíveis:
  • Código de máquina nativo (compilação completa)
  • Bytecode para máquinas virtuais (como JVM ou Python)
  • Código intermediário como Three-Address Code (TAC)
  • Tradução para outra linguagem de alto nível (transpilação)
Para nosso projeto, implementaremos a geração de código em formato TAC (Three-Address Code), uma representação intermediária simples e eficiente.
Na geração de código, traduzimos estruturas de alto nível para instruções mais simples que podem ser executadas diretamente ou convertidas para código de máquina.
Three-Address Code (TAC)
O TAC é um formato de código intermediário onde cada instrução tem no máximo três endereços (dois operandos e um resultado). Este formato simplifica a geração de código e facilita otimizações.
Código Fonte
a = b * c + d * e
Código TAC
t1 = b * c t2 = d * e t3 = t1 + t2 a = t3
Benefícios
  • Instruções simples e uniformes
  • Facilita análise de fluxo de dados
  • Simplifica otimizações
  • Independente de arquitetura
Cada instrução TAC representa uma operação atômica, tornando explícito o fluxo de execução e facilitando a tradução para código de máquina específico.
Implementação do Gerador de Código TAC
class TACGenerator: def __init__(self): self.instructions = [] self.temp_counter = 0 self.label_counter = 0 def new_temp(self): """Cria um novo nome de variável temporária""" temp = f"t{self.temp_counter}" self.temp_counter += 1 return temp def new_label(self): """Cria um novo rótulo para saltos""" label = f"L{self.label_counter}" self.label_counter += 1 return label def generate(self, ast): """Gera código TAC a partir da AST""" return self._generate_node(ast) def _generate_node(self, node): if node is None: return None if node.node_type == 'BinaryOp': # Gera código para os operandos left_result = self._generate_node(node.left) right_result = self._generate_node(node.right) # Cria temporário para o resultado result = self.new_temp() # Adiciona instrução TAC self.instructions.append(f"{result} = {left_result} {node.op} {right_result}") return result elif node.node_type == 'Num': return str(node.value) elif node.node_type == 'Var': return node.name # Implementação para outros tipos de nós...
Este gerador percorre a AST e cria instruções TAC para cada nó, usando variáveis temporárias para armazenar resultados intermediários.
Gerando Código para Estruturas de Controle
Código para Estrutura If-Else
if (a > b) { x = 1; } else { x = 2; }
Transformação em TAC:
if_temp = a > b if_false if_temp goto L1 x = 1 goto L2 L1: x = 2 L2:
Código para Loop While
while (i < 10) { sum = sum + i; i = i + 1; }
Transformação em TAC:
L1: temp = i < 10 if_false temp goto L2 sum = sum + i i = i + 1 goto L1 L2:
Observe como usamos rótulos (labels) e instruções de salto condicional para implementar estruturas de controle de fluxo em TAC.
Otimizações Básicas de Código
Otimizações são transformações no código que melhoram sua eficiência sem alterar seu comportamento. Algumas otimizações básicas que podemos implementar:
Propagação de Constantes
Substitui variáveis por seus valores constantes conhecidos em tempo de compilação.
x = 5 y = x + 2 → y = 5 + 2 → y = 7
Eliminação de Código Morto
Remove código que nunca é executado ou cujo resultado nunca é usado.
if (false) { x = 10; // código morto, nunca executado }
Dobramento de Constantes
Calcula expressões com constantes em tempo de compilação.
x = 5 * 3 + 2 → x = 15 + 2 → x = 17
Simplificação Algébrica
Simplifica expressões usando propriedades matemáticas.
x = y * 1 → x = y x = y + 0 → x = y
Implementando Otimizações
def optimize_tac(instructions): """Aplica otimizações básicas no código TAC""" optimized = [] constants = {} # Mapeia variáveis para seus valores constantes for instr in instructions: # Analisa a instrução if '=' in instr: dest, expr = instr.split('=', 1) dest = dest.strip() expr = expr.strip() # Propagação de constantes for var, value in constants.items(): expr = expr.replace(var, value) # Dobramento de constantes try: # Tenta avaliar a expressão se for constante result = str(eval(expr)) constants[dest] = result optimized.append(f"{dest} = {result}") except: # Se não for constante, mantém a instrução optimized.append(f"{dest} = {expr}") # Se não for uma expressão constante, remove do mapeamento if dest in constants: del constants[dest] else: # Instruções que não são atribuições optimized.append(instr) return optimized
Esta é uma implementação simplificada que realiza propagação e dobramento de constantes. Na prática, otimizações mais sofisticadas exigiriam análise de fluxo de dados e algoritmos mais complexos.
Exemplo de Otimização
Código Original
int a = 5; int b = 10; int c = a + b; int d = c * 2; if (a > 0) { int e = c + d; print(e); }
Código TAC Não Otimizado
a = 5 b = 10 c = a + b d = c * 2 temp1 = a > 0 if_false temp1 goto L1 e = c + d print e L1:
Código TAC Otimizado
a = 5 b = 10 c = 15 // a + b calculado em tempo de compilação d = 30 // c * 2 calculado em tempo de compilação temp1 = 1 // 5 > 0 avaliado como verdadeiro (1) // if_false removido pois condição é sempre verdadeira e = 45 // c + d calculado em tempo de compilação print 45 // valor de e propagado // L1 removido pois é código morto
Observe como várias otimizações foram aplicadas: dobramento de constantes, propagação de constantes e eliminação de código morto.
Emissão de Código Final
Após a geração e otimização do código TAC, precisamos emitir o código final no formato desejado. Para nosso projeto, vamos gerar um arquivo Python equivalente, que pode ser executado diretamente:
def emit_python(tac_instructions): """Converte código TAC para Python""" python_code = [] python_code.append("# Código gerado automaticamente") python_code.append("# por nosso compilador") for instr in tac_instructions: if '=' in instr and not ('==' in instr or '<=' in instr or '>=' in instr): # Atribuição simples python_code.append(instr) elif instr.startswith('if_false'): # Instrução condicional parts = instr.split() condition = parts[1] label = parts[3] python_code.append(f"if not {condition}:") python_code.append(f" goto {label}") elif instr.startswith('goto'): # Instrução de salto label = instr.split()[1] python_code.append(f"# goto {label}") elif instr.startswith('L'): # Rótulo python_code.append(f"# {instr}:") elif instr.startswith('print'): # Instrução print var = instr.split()[1] python_code.append(f"print({var})") else: # Outras instruções python_code.append(f"# {instr}") return "\n".join(python_code)
A emissão de código final transforma nosso código intermediário TAC em um formato executável, seja Python, C, ou até mesmo assembly.
Dependendo da complexidade do projeto, podemos optar por diferentes abordagens:
  • Tradução para linguagem de alto nível (transpilação)
  • Geração de bytecode para uma máquina virtual
  • Geração direta de código de máquina
Para fins didáticos, a tradução para Python é mais simples e permite visualizar facilmente o resultado da compilação.
Integração Completa dos Módulos
1
Analisador Léxico
Transforma o código fonte em uma sequência de tokens, identificando lexemas, categorias e posições.
# lexer.py def tokenize(source_code): # implementação... return tokens
2
Analisador Sintático
Processa os tokens e constrói uma árvore sintática abstrata (AST) conforme a gramática da linguagem.
# parser.py def parse(tokens): # implementação... return ast
3
Analisador Semântico
Verifica se a AST está semanticamente correta, validando tipos, escopos e regras de uso.
# semantic.py def analyze(ast): # implementação... return symbol_table
4
Gerador de Código
Transforma a AST validada em código intermediário TAC e depois em código final executável.
# codegen.py def generate(ast, symbol_table): # implementação... return code
O arquivo principal (compiler.py) orquestra todos esses módulos, processando o arquivo de entrada e gerando o arquivo de saída com o código compilado.
Pipeline Completo do Compilador
# compiler.py import lexer import parser import semantic import codegen import optimizer import os def compile_file(input_file, output_file): """Compila um arquivo fonte para o arquivo de saída""" try: # Lê o arquivo fonte with open(input_file, 'r') as f: source_code = f.read() # Pipeline de compilação print(f"Compilando {input_file}...") # 1. Análise léxica print("Realizando análise léxica...") tokens = lexer.tokenize(source_code) # 2. Análise sintática print("Realizando análise sintática...") ast = parser.parse(tokens) # 3. Análise semântica print("Realizando análise semântica...") symbol_table = semantic.analyze(ast) # 4. Geração de código TAC print("Gerando código intermediário...") tac_code = codegen.generate_tac(ast, symbol_table) # 5. Otimização print("Otimizando código...") optimized_tac = optimizer.optimize(tac_code) # 6. Emissão de código final print("Gerando código final...") final_code = codegen.emit_python(optimized_tac) # Escreve o código no arquivo de saída with open(output_file, 'w') as f: f.write(final_code) print(f"Compilação concluída. Código gerado em {output_file}") return True except Exception as e: print(f"Erro durante a compilação: {e}") return False if __name__ == "__main__": import sys if len(sys.argv) != 3: print("Uso: python compiler.py arquivo_entrada.src arquivo_saida.py") else: compile_file(sys.argv[1], sys.argv[2])
Este script integra todos os componentes do compilador em um único pipeline, processando o código fonte desde a análise léxica até a geração do código final.
Tratamento de Erros Integrado
Um bom compilador deve fornecer mensagens de erro claras e informativas. Vamos implementar um sistema de tratamento de erros unificado:
class CompilationError(Exception): def __init__(self, phase, message, line=None, column=None): self.phase = phase # 'lexical', 'syntax', 'semantic', 'codegen' self.message = message self.line = line self.column = column super().__init__(self.format_message()) def format_message(self): location = "" if self.line is not None: location += f" na linha {self.line}" if self.column is not None: location += f", coluna {self.column}" return f"Erro {self.phase}{location}: {self.message}" # No pipeline de compilação: try: # Código de compilação pass except CompilationError as e: print(e) # Registra o erro em um log with open('compile_errors.log', 'a') as log: log.write(f"{datetime.now()}: {str(e)}\n")
Com esta abordagem, todos os módulos podem lançar exceções de forma padronizada, facilitando o diagnóstico e a correção de problemas pelo usuário.
Exemplo Completo: Da Entrada à Saída
Código Fonte (entrada.src)
programa { int a = 5; int b = 10; // Calcula a soma int soma = a + b; // Verifica se é maior que 10 if (soma > 10) { print("A soma é maior que 10"); print(soma); } else { print("A soma é menor ou igual a 10"); } }
Código Python Gerado (saida.py)
# Código gerado automaticamente # por nosso compilador a = 5 b = 10 soma = a + b temp1 = soma > 10 if not temp1: goto L1 print("A soma é maior que 10") print(soma) goto L2 # L1: print("A soma é menor ou igual a 10") # L2:
Este exemplo mostra como um programa simples em nossa linguagem é compilado para Python. Note que o código gerado ainda contém estruturas TAC como rótulos e gotos, que não são nativos do Python. Em uma implementação completa, essas estruturas seriam traduzidas para construções Python adequadas.
Compilando Estruturas de Controle
Estruturas Condicionais
Implementamos condicionais com instruções de salto e rótulos:
// Condicional if-else if (condição) { bloco1 } else { bloco2 } // Código TAC gerado temp = condição if_false temp goto L1 código_bloco1 goto L2 L1: código_bloco2 L2:
Estruturas de Repetição
Loops são implementados com saltos para trás:
// Loop while while (condição) { bloco } // Código TAC gerado L1: temp = condição if_false temp goto L2 código_bloco goto L1 L2:
Estas transformações mostram como estruturas de alto nível são convertidas em instruções mais simples usando saltos condicionais e incondicionais no código TAC.
Testes e Depuração
Testes são essenciais para garantir que cada componente do compilador funcione corretamente. Podemos implementar testes unitários para cada módulo e testes de integração para o pipeline completo:
import unittest from compiler import lexer, parser, semantic, codegen class TestLexer(unittest.TestCase): def test_tokenize(self): code = "int x = 5;" tokens = lexer.tokenize(code) self.assertEqual(len(tokens), 5) self.assertEqual(tokens[0].type, "TYPE") self.assertEqual(tokens[0].value, "int") class TestParser(unittest.TestCase): def test_parse_declaration(self): tokens = [ Token("TYPE", "int", 1), Token("ID", "x", 1), Token("ASSIGN", "=", 1), Token("NUMBER", "5", 1), Token("SEMICOLON", ";", 1) ] ast = parser.parse(tokens) self.assertEqual(ast.node_type, "Program") self.assertEqual(len(ast.statements), 1) self.assertEqual(ast.statements[0].node_type, "VarDecl") # Mais testes para semântica e geração de código... if __name__ == "__main__": unittest.main()
Para depuração, podemos adicionar flags e níveis de verbosidade:
# No compilador principal def compile_file(input_file, output_file, debug=False): if debug: print("Modo de depuração ativado") # Durante a análise léxica if debug: print("Tokens gerados:") for token in tokens: print(f" {token}") # Durante a análise sintática if debug: print("AST gerada:") print_ast(ast, indent=2) # E assim por diante...
Ferramentas como debuggers, logs detalhados e visualizadores de AST podem ajudar a identificar e corrigir problemas no compilador.
Desafio: Compilando um Programa Complexo
Vamos testar nosso compilador com um programa mais complexo que utiliza várias estruturas:
programa { // Função para calcular fatorial int fatorial(int n) { if (n <= 1) { return 1; } else { return n * fatorial(n - 1); } } // Função para verificar se um número é primo bool ehPrimo(int num) { if (num <= 1) { return false; } int i = 2; while (i * i <= num) { if (num % i == 0) { return false; } i = i + 1; } return true; } // Programa principal int numero = 5; print("Fatorial de " + numero + " é " + fatorial(numero)); if (ehPrimo(numero)) { print(numero + " é primo"); } else { print(numero + " não é primo"); } }
Este programa testa recursão, estruturas condicionais, loops, operações aritméticas e funções. Compile-o com seu compilador e verifique se o resultado é correto.
Funcionalidades Avançadas (Extensões)
Inferência de Tipos
Implementar inferência automática de tipos usando análise de fluxo de dados, permitindo declarações como "var x = 5" sem especificar o tipo explicitamente.
Otimizações Avançadas
Adicionar otimizações mais sofisticadas como eliminação de subexpressões comuns, análise de liveness e scheduling de instruções.
Debugging
Adicionar informações de debugging no código gerado, permitindo rastrear a execução de volta ao código fonte original.
Garbage Collection
Implementar gerenciamento automático de memória para alocações dinâmicas em linguagens que suportam heap.
Compilação JIT
Implementar compilação Just-In-Time para otimizar trechos críticos durante a execução do programa.
Paralelização
Identificar automaticamente trechos paralelizáveis e gerar código que aproveite múltiplos núcleos/threads.
Estas extensões podem ser implementadas como projetos adicionais após a conclusão do compilador básico. Cada uma delas oferece oportunidades para aprofundar o conhecimento em áreas específicas da compilação.
Sugestões para o Projeto Final
1
Defina o escopo com clareza
Estabeleça quais características da linguagem serão suportadas pelo seu compilador. Comece com um subconjunto mínimo e adicione recursos conforme o tempo permitir.
2
Divida o trabalho eficientemente
Se estiver trabalhando em equipe, divida os módulos de forma que cada membro possa trabalhar em paralelo. Use APIs bem definidas entre os módulos para facilitar a integração.
3
Priorize a correção
É melhor ter um compilador simples que funciona corretamente do que um compilador complexo com bugs. Implemente testes desde o início e use TDD (Test-Driven Development).
4
Documente seu trabalho
Mantenha documentação clara sobre as decisões de design, a gramática da linguagem, as estruturas de dados e os algoritmos utilizados. Isso facilitará a manutenção e expansão futura.
Preparando a Apresentação Final
Na próxima aula, você apresentará seu projeto de compilador. Sua apresentação deve incluir:
  1. Introdução à linguagem implementada (características, sintaxe)
  1. Arquitetura do compilador (módulos, fluxo de dados)
  1. Demonstração com exemplos de código
  1. Desafios enfrentados e soluções adotadas
  1. Limitações atuais e possíveis extensões futuras
Prepare exemplos de código que demonstrem diferentes aspectos do seu compilador, desde estruturas simples até construções mais complexas. Mostre o código fonte, o processo de compilação e a execução do código gerado.
Dicas para uma apresentação eficaz:
  • Ensaie a demonstração para evitar surpresas
  • Prepare-se para explicar decisões técnicas
  • Distribua o tempo igualmente entre os membros da equipe
  • Destaque os aspectos mais inovadores do seu projeto
  • Esteja pronto para responder perguntas técnicas
Conclusão e Próximos Passos
O que aprendemos
Desenvolvemos um compilador completo com análise léxica, sintática, semântica e geração de código. Exploramos conceitos teóricos e implementamos soluções práticas para problemas reais de compilação.
Habilidades adquiridas
Além dos conhecimentos específicos sobre compiladores, desenvolvemos habilidades em processamento de linguagens, estruturas de dados, algoritmos e técnicas de engenharia de software.
Aplicações práticas
Os conceitos aprendidos podem ser aplicados em interpretadores, transpiladores, validadores de código, formatadores, linters e outras ferramentas de processamento de linguagens.
Próximos passos
Continue expandindo seu compilador com novas funcionalidades, otimizações mais avançadas ou suporte a outras linguagens. Considere contribuir para projetos open source de compiladores.
Lembre-se que a construção de compiladores é uma das áreas mais fundamentais e desafiadoras da ciência da computação. O conhecimento adquirido neste projeto será valioso em muitas outras áreas da sua carreira.