Ao projetar uma linguagem de programação própria, quando faz sentido escrever um conversor que pegue o código-fonte e o converta em código C ou C ++ para que eu possa usar um compilador existente como o gcc para finalizar o código da máquina? Existem projetos que usam essa abordagem?
34
Respostas:
A tradução para o código C é um hábito muito bem estabelecido. O C original com classes (e as implementações iniciais do C ++, então chamadas Cfront ) fizeram isso com êxito. Várias implementações do Lisp ou Scheme estão fazendo isso, por exemplo, Chicken Scheme , Scheme48 , Bigloo . Algumas pessoas traduzido Prolog de C . E o mesmo aconteceu com algumas versões do Mozart (e houve tentativas de compilar o código de código Ocaml para C ). O sistema CAIA de inteligência artificial da J.Pitrat também é inicializado e gera todo o seu código C. Vala também traduz para C, para código relacionado ao GTK. O livro de Queinnec, Lisp In Small Pieces tem algum capítulo sobre tradução para C.
Um dos problemas ao traduzir para C são as chamadas recursivas de cauda . O padrão C não garante que um compilador C os traduza adequadamente (para um "salto com argumentos", ou seja, sem comer pilha de chamadas), mesmo que em alguns casos, versões recentes do GCC (ou do Clang / LLVM) façam essa otimização .
Outra questão é a coleta de lixo . Várias implementações apenas usam o coletor de lixo conservador Boehm (que é compatível com C ...). Se você quisesse coletar o código de coleta de lixo (como várias implementações do Lisp, por exemplo, SBCL), isso pode ser um pesadelo (você gostaria
dlclose
no Posix).Outra questão é lidar com continuações de primeira classe e call / cc . Mas truques inteligentes são possíveis (veja o esquema de galinhas). Acessar a pilha de chamadas pode exigir muitos truques (mas consulte o GNU backtrace , etc ....). A persistência ortogonal de continuações (ou seja, de pilhas ou fios) seria difícil em C.
Manipulação de exceção geralmente é uma questão de emitir chamadas inteligentes para longjmp etc ...
Você pode gerar (no seu código C emitido)
#line
diretivas apropriadas . Isso é chato e exige muito trabalho (por exemplo, você deve produzirgdb
código mais facilmente debocável).Meu idioma específico do domínio lispy MELT (para personalizar ou estender o GCC ) é traduzido para C (na verdade, para C ++ ruim agora). Ele possui seu próprio coletor de lixo para cópia geracional. (Você pode estar interessado por Qish ou Ravenbrook MPS ). Na verdade, o GC geracional é mais fácil no código C gerado pela máquina do que no código C escrito à mão (porque você personalizará o seu gerador de código C para o seu equipamento de barreira contra gravação e GC).
Não conheço nenhuma implementação de linguagem traduzida para código C ++ genuíno , ou seja, usando alguma técnica de "coleta de lixo em tempo de compilação" para emitir código C ++ usando muitos modelos de STL e respeitando o idioma RAII . (por favor, diga se você conhece um).
O que é engraçado hoje é que (nos desktops atuais do Linux) os compiladores C podem ser rápidos o suficiente para implementar um loop interativo de leitura-avaliação-impressão traduzido para C: você emitirá código C (algumas centenas de linhas) a cada usuário interação, você
fork
a compilará em um objeto compartilhado, o que você faria entãodlopen
. (O MELT está fazendo isso tudo pronto, e geralmente é rápido o suficiente). Tudo isso pode levar alguns décimos de segundo e ser aceitável pelos usuários finais.Quando possível, eu recomendaria a tradução para C, não para C ++, principalmente porque a compilação em C ++ é lenta.
Se você estiver implementando sua linguagem, também poderá considerar (em vez de emitir código C) algumas bibliotecas JIT como libjit , GNU lightning , asmjit ou mesmo LLVM ou GCCJIT . Se você deseja traduzir para C, às vezes pode usar tinycc : ele compila muito rapidamente o código C gerado (mesmo na memória) para diminuir o código da máquina. Mas, em geral, você deseja aproveitar as otimizações feitas por um compilador C real como o GCC
Se você traduzir para C seu idioma, certifique-se de criar o AST inteiro do código C gerado na memória primeiro (isso também facilita a geração de todas as declarações e de todas as definições e códigos de função). Você seria capaz de fazer algumas otimizações / normalizações dessa maneira. Além disso, você pode estar interessado em várias extensões do GCC (por exemplo, gotos computados). Você provavelmente desejará evitar a geração de grandes funções C - por exemplo, centenas de milhares de linhas de C geradas - (é melhor dividi-las em partes menores), pois a otimização de compiladores C é muito infeliz com funções C muito grandes (na prática, e experimentalmente,
gcc -O
tempo de compilação de funções grandes é proporcional ao quadrado do tamanho do código da função). Portanto, limite o tamanho das funções C geradas a alguns milhares de linhas cada.Observe que os compiladores Clang (através de LLVM ) e GCC (através de libgccjit ) oferecem alguma maneira de emitir algumas representações internas adequadas para esses compiladores, mas fazer isso pode (ou não) ser mais difícil do que emitir código C (ou C ++), e é específico para cada compilador.
Se você estiver projetando um idioma para ser traduzido para C, provavelmente precisará de vários truques (ou construções) para gerar uma mistura de C com seu idioma. Meu artigo sobre DSL2011 MELT: um idioma específico do domínio traduzido incorporado no compilador GCC deve fornecer dicas úteis.
fonte
Faz sentido quando o tempo para gerar o código completo da máquina supera a inconveniência de ter uma etapa intermediária de compilar sua "IL" no código da máquina usando um compilador C.
Normalmente, as linguagens específicas do domínio são escritas dessa maneira, um sistema de nível muito alto é usado para definir ou descrever um processo que é compilado em um executável ou dll. O tempo gasto para produzir montagem boa / em funcionamento é muito maior do que gerar C, e C é bastante próximo do código de montagem para desempenho, portanto, faz sentido gerar C e reutilizar as habilidades dos escritores do compilador C. Observe que não é apenas compilação, mas também otimização - os caras que escrevem gcc ou llvm gastam muito tempo criando código de máquina otimizado; seria estúpido tentar reinventar todo o seu trabalho duro.
Pode ser mais aceitável reutilizar o back-end do compilador do LLVM, que o IIRC é neutro em termos de idioma, para que você gere instruções do LLVM em vez do código C.
fonte
Escrever um compilador para produzir código de máquina pode não ser muito mais difícil do que escrever um que produz C (em alguns casos, pode ser mais fácil), mas um compilador que produz código de máquina só poderá produzir programas executáveis na plataforma específica para a qual foi escrito; um compilador que produz código C, por outro lado, pode ser capaz de produzir programas para qualquer plataforma que use um dialeto de C que o código gerado foi projetado para suportar. Observe que, em muitos casos, pode ser possível escrever código C que seja completamente portátil e que se comportará conforme desejado, sem o uso de comportamentos não garantidos pelo padrão C, mas o código que se baseia em comportamentos garantidos pela plataforma poderá executar muito mais rapidamente em plataformas que oferecem essas garantias do que o código que não.
Por exemplo, suponha que um idioma suporte um recurso para gerar a
UInt32
partir de quatro bytes consecutivos de um alinhado arbitrariamenteUInt8[]
, interpretado da maneira big endian. Em alguns compiladores, pode-se escrever o código como:e faça com que o compilador gere uma operação de carregamento de palavras seguida por uma instrução de bytes reversos na palavra. Alguns compiladores, no entanto, não suportariam o modificador __packed e, na sua ausência, gerariam código que não funcionaria.
Como alternativa, pode-se escrever o código como:
esse código deve funcionar em qualquer plataforma, mesmo naquelas em que
CHAR_BITS
não é 8 (supondo que cada octeto de dados de origem tenha terminado em um elemento de matriz distinto), mas esse código pode provavelmente não ser executado tão rápido quanto seria o não-portátil versão em plataformas que suportam o primeiro.Observe que a portabilidade geralmente exige que o código seja extremamente liberal com previsões de tipos e construções semelhantes. Por exemplo, o código que deseja multiplicar dois números inteiros não assinados de 32 bits e gerar os 32 bits inferiores do resultado deve ser portável para:
Sem isso
1u
, um compilador em um sistema em que INT_BITS variava de 33 a 64 poderia legitimamente fazer o que quisesse se o produto de xey fosse maior que 2.147.483.647, e alguns compiladores tendem a aproveitar essas oportunidades.fonte
Você tem algumas excelentes respostas acima, mas, em um comentário, respondeu à pergunta "Por que você deseja criar uma linguagem de programação própria?" Com "Seria principalmente para fins de aprendizado". vou responder de um ângulo diferente.
Faz sentido escrever um conversor que pega o código-fonte e o converte em código C ou C ++, para que você possa usar um compilador existente como o gcc para terminar com o código da máquina, se estiver mais interessado em aprender lexical, sintaxe e análise semântica do que você está aprendendo sobre geração e otimização de código!
Escrever o seu próprio gerador de código de máquina é um trabalho bastante significativo que você pode evitar ao compilar o código C, se não for nisso que você está mais interessado!
Se, no entanto, você está no programa de montagem e fascinado pelos desafios de otimizar o código no nível mais baixo, escreva você mesmo um gerador de código para a experiência de aprendizado!
fonte
Depende do sistema operacional que você estiver usando, se estiver usando o Windows. Existe um Microsoft IL (idioma intermediário) que converte seu código em idioma intermediário para que não demore muito tempo para ser compilado em código de máquina. Ou, se você estiver usando Linux, existe um compilador separado para esse
Voltando à sua pergunta, quando você cria o seu próprio idioma, deve ter um compilador ou intérprete separado para isso, porque a máquina não conhece o idioma de alto nível. Seu código deve ser compilado no código da máquina para torná-lo útil para a máquina
fonte
Your code should be compiled into machine code to make it useful for machine
- Se o seu compilador produziu o código c como saída, você pode colocá-lo no compilador CA para produzir o código da máquina, certo?