Compiladores avançados, como gcc
códigos de compilação em arquivos legíveis por máquina, de acordo com o idioma em que o código foi gravado (por exemplo, C, C ++, etc). De fato, eles interpretam o significado de cada código de acordo com a biblioteca e as funções dos idiomas correspondentes. Corrija-me se eu estiver errado.
Desejo entender melhor os compiladores escrevendo um compilador muito básico (provavelmente em C) para compilar um arquivo estático (por exemplo, Hello World em um arquivo de texto). Eu tentei alguns tutoriais e livros, mas todos são para casos práticos. Eles lidam com a compilação de códigos dinâmicos com significados conectados ao idioma correspondente.
Como escrever um compilador básico para converter um texto estático em um arquivo legível por máquina?
O próximo passo será a introdução de variáveis no compilador; imagine que queremos escrever um compilador que compile apenas algumas funções de uma linguagem.
A introdução de tutoriais e recursos práticos é muito apreciada :-)
fonte
Respostas:
Introdução
Um compilador típico executa as seguintes etapas:
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
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:
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.
fonte
Let's Build a Compiler , de Jack Crenshaw , embora inacabado, é uma introdução e um tutorial eminentemente legíveis.
A Construção de Compilador de Nicklaus Wirth é um livro muito bom sobre os fundamentos da construção simples de compilador. Ele se concentra na descida recursiva de cima para baixo, o que, convenhamos, é MUITO mais fácil do que lex / yacc ou flex / bison. O compilador PASCAL original que seu grupo escreveu foi feito dessa maneira.
Outras pessoas mencionaram os vários livros de Dragon.
fonte
Na verdade, eu começaria escrevendo um compilador para o Brainfuck . É uma linguagem bastante obtusa para programar, mas possui apenas 8 instruções para implementar. É o mais simples possível e existem instruções C equivalentes para os comandos envolvidos, se você achar a sintaxe desanimadora.
fonte
Se você realmente deseja escrever apenas código legível por máquina e não direcionado a uma máquina virtual, precisará ler os manuais da Intel e entender
uma. Vinculando e carregando código executável
b. Formatos COFF e PE (para Windows), como alternativa, entender o formato ELF (para Linux)
Muito mais difícil do que foi dito. Sugiro que você leia Compiladores e Intérpretes em C ++ como ponto de partida (por Ronald Mak). Como alternativa, "vamos criar um compilador" por Crenshaw é OK.
Se você não quiser fazer isso, também poderá escrever sua própria VM e escrever um gerador de código direcionado para essa VM.
Dicas: Aprenda Flex e Bison PRIMEIRO. Em seguida, crie seu próprio compilador / VM.
Boa sorte!
fonte
A abordagem DIY para compilador simples pode ser assim (pelo menos é assim que meu projeto uni era):
Deve haver muita literatura descrevendo cada etapa em detalhes.
fonte