Introdução
Um compilador típico executa as seguintes etapas:
- Análise: o texto de origem é convertido em uma árvore de sintaxe abstrata (AST).
- Resolução de referências a outros módulos (C adia essa etapa até a vinculação).
- Validação semântica: eliminar declarações sintaticamente corretas que não fazem sentido, por exemplo, código inacessível ou declarações duplicadas.
- Transformações equivalentes e otimização de alto nível: o AST é transformado para representar uma computação mais eficiente com a mesma semântica. Isso inclui, por exemplo, cálculo antecipado de subexpressões comuns e expressões constantes, eliminando atribuições locais excessivas (consulte também SSA ), etc.
- Geração de código: o AST é transformado em código linear de baixo nível, com saltos, alocação de registros e similares. Algumas chamadas de função podem ser incorporadas nesse estágio, alguns loops desenrolados etc.
- Otimização do olho mágico: o código de baixo nível é verificado em busca de ineficiências locais simples que são eliminadas.
A maioria dos compiladores modernos (por exemplo, gcc e clang) repete as duas últimas etapas mais uma vez. Eles usam uma linguagem intermediária de baixo nível, mas independente de plataforma, para a geração inicial de código. Em seguida, esse idioma é convertido em código específico da plataforma (x86, ARM etc.), fazendo aproximadamente a mesma coisa de uma maneira otimizada para a plataforma. Isso inclui, por exemplo, o uso de instruções vetoriais, quando possível, reordenação de instruções para aumentar a eficiência da previsão de ramificação e assim por diante.
Depois disso, o código do objeto está pronto para a vinculação. A maioria dos compiladores de código nativo sabe como chamar um vinculador para produzir um executável, mas não é uma etapa de compilação propriamente dita. Em linguagens como Java e C #, a vinculação pode ser totalmente dinâmica, feita pela VM no momento do carregamento.
Lembre-se do básico
- Faça funcionar
- Faça bonito
- Torne-o eficiente
Essa sequência clássica se aplica a todo o desenvolvimento de software, mas exige repetição.
Concentre-se no primeiro passo da sequência. Crie a coisa mais simples que poderia funcionar.
Leia os livros!
Leia o Livro do Dragão de Aho e Ullman. Isso é clássico e ainda é bastante aplicável hoje.
O design moderno do compilador também é elogiado.
Se esse material é muito difícil para você no momento, leia algumas introduções sobre a análise primeiro; as bibliotecas de análise geralmente incluem introduções e exemplos.
Certifique-se de trabalhar com gráficos, especialmente árvores. Essas coisas são as coisas que os programas são feitos no nível lógico.
Defina bem o seu idioma
Use a notação que desejar, mas verifique se você tem uma descrição completa e consistente do seu idioma. Isso inclui sintaxe e semântica.
Está na hora de escrever trechos de código em seu novo idioma como casos de teste para o futuro compilador.
Use seu idioma favorito
Não há problema em escrever um compilador em Python ou Ruby ou qualquer outra linguagem que seja fácil para você. Use algoritmos simples que você entende bem. A primeira versão não precisa ser rápida, eficiente ou com recursos completos. Ele só precisa estar correto o suficiente e fácil de modificar.
Também é bom escrever diferentes estágios de um compilador em diferentes idiomas, se necessário.
Prepare-se para escrever muitos testes
Seu idioma inteiro deve ser coberto por casos de teste; efetivamente será definido por eles. Familiarize-se com sua estrutura de teste preferida. Faça testes desde o primeiro dia. Concentre-se nos testes 'positivos' que aceitam o código correto, em vez da detecção de código incorreto.
Execute todos os testes regularmente. Corrija os testes quebrados antes de continuar. Seria uma pena acabar com uma linguagem mal definida que não pode aceitar código válido.
Crie um bom analisador
Geradores de analisadores são muitos . Escolha o que quiser. Você também pode escrever seu próprio analisador a partir do zero, mas só vale a pena se a sintaxe de sua língua é morto simples.
O analisador deve detectar e relatar erros de sintaxe. Escreva muitos casos de teste, positivos e negativos; reutilize o código que você escreveu ao definir o idioma.
A saída do seu analisador é uma árvore de sintaxe abstrata.
Se o seu idioma tiver módulos, a saída do analisador pode ser a representação mais simples do 'código de objeto' gerado. Existem várias maneiras simples de despejar uma árvore em um arquivo e carregá-la rapidamente.
Crie um validador semântico
Muito provavelmente, seu idioma permite construções sintaticamente corretas que podem não fazer sentido em determinados contextos. Um exemplo é uma declaração duplicada da mesma variável ou a passagem de um parâmetro de um tipo errado. O validador detectará esses erros olhando para a árvore.
O validador também resolverá referências a outros módulos escritos em seu idioma, carregará esses outros módulos e utilizará no processo de validação. Por exemplo, esta etapa garantirá que o número de parâmetros passados para uma função de outro módulo esteja correto.
Novamente, escreva e execute muitos casos de teste. Casos triviais são tão indispensáveis na solução de problemas quanto inteligentes e complexos.
Gerar código
Use as técnicas mais simples que você conhece. Geralmente, não há problema em traduzir diretamente uma construção de linguagem (como uma if
instrução) em um modelo de código pouco parametrizado, não muito diferente de um modelo HTML.
Mais uma vez, ignore a eficiência e concentre-se na correção.
Segmente uma VM de baixo nível independente de plataforma
Suponho que você ignore coisas de baixo nível, a menos que esteja profundamente interessado em detalhes específicos de hardware. Esses detalhes são sangrentos e complexos.
Suas opções:
- LLVM: permite geração eficiente de código de máquina, geralmente para x86 e ARM.
- CLR: direciona-se ao .NET, principalmente x86 / Windows; tem um bom JIT.
- JVM: tem como alvo o mundo Java, bastante multiplataforma, tem um bom JIT.
Ignorar otimização
A otimização é difícil. Quase sempre a otimização é prematura. Gere código ineficiente, mas correto. Implemente o idioma inteiro antes de tentar otimizar o código resultante.
Obviamente, otimizações triviais podem ser introduzidas. Mas evite qualquer coisa esperta e cabeluda antes que seu compilador esteja estável.
E daí?
Se tudo isso não for muito intimidador para você, continue! Para um idioma simples, cada uma das etapas pode ser mais simples do que você imagina.
Ver um 'Hello world' a partir de um programa que seu compilador criou pode valer a pena.