Quando faz sentido compilar meu próprio idioma para o código C primeiro?


34

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?



4
Se você olhar além do C, verá que C # e Java também são compilados para linguagens intermediárias. Você é salvo de ter que refazer muito trabalho que outra pessoa já fez, visando um idioma intermediário, em vez de ir direto para a montagem.
Casey

1
@emodendroket No entanto, C # e Java são compilados para uma IL projetada para ser uma IL em geral e especificamente para C # / Java; portanto, de muitas maneiras, o bytecode CIL e JVM são mais sensíveis e convenientes que uma IL do que C jamais poderia ser. Não se trata de usar qualquer linguagem intermediária, mas sobre qual linguagem intermediária usar.

1
Veja várias implementações de software livre que geram código C. E espero que você faça o seu software de implementação de linguagem grátis.
Basile Starynkevitch

2
Aqui está o link atualizado do comentário de @ RobertHarvey: yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean

Respostas:


52

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 dlcloseno 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) #linediretivas apropriadas . Isso é chato e exige muito trabalho (por exemplo, você deve produzir gdbcó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ê forka compilará em um objeto compartilhado, o que você faria então dlopen. (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 -Otempo 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.


Você está se referindo a "Chicken Scheme?"
Robert Harvey

1
Sim. Eu dei o URL.
Basile Starynkevitch

É relativamente prático criar uma máquina virtual, como Java ou algo assim, compilar bytecode para C e usar o gcc para compilação JIT? Ou eles deveriam ir direto do bytecode para a montagem?
Panzercrisis

1
@ Panzercrisis A maioria dos compiladores JIT exige seus back-ends de código de máquina para dar suporte a coisas como substituir uma função e corrigir o código existente por uma porta de pulo / interceptação. Além disso, o gcc é ... arquitetonicamente menos adequado para a compilação JIT e outros casos de uso. No entanto, consulte libgccjit: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html e gcc.gnu.org/wiki/JIT

1
Excelente material de orientação. Obrigado!
capr 15/02

7

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.


Parece que as bibliotecas são uma razão bastante convincente para considerar isso também.
Casey

Quando você diz "seu 'IL'", a que você está se referindo? Uma árvore de sintaxe abstrata?
Robert Harvey

@RobertHarvey não, quero dizer código C. No caso dos OPs, esse é um idioma intermediário a meio caminho entre seu próprio idioma de alto nível e o código da máquina. Eu colocá-lo entre aspas para tentar transmitir a ideia de que a sua não IL como o usado por muitas pessoas (ou seja, .NET IL da Microsoft, por exemplo)
gbjbaanb

2

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 UInt32partir de quatro bytes consecutivos de um alinhado arbitrariamente UInt8[], interpretado da maneira big endian. Em alguns compiladores, pode-se escrever o código como:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

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:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

esse código deve funcionar em qualquer plataforma, mesmo naquelas em que CHAR_BITSnã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:

uint32_t result = 1u*x*y;

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.


1

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!


-7

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


2
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?
Robert Harvey

sim. porque máquina não faz a linguagem c
Tayyab Gulsher Vohra

2
Direita. Portanto, a pergunta era "Quando faz sentido emitir ce usar o compilador CA, em vez de emitir linguagem de máquina ou código de bytes diretamente?"
Robert Harvey

na verdade, ele está pedindo para projetar sua linguagem de programação na qual está pedindo que "a converta em código C ou C ++". Então, eu estou explicando isso se você está criando sua própria linguagem de programação, por que você deve usar o compilador c ou c ++. se você é inteligente o suficiente, você deve criar o seu próprio
Tayyab Gulsher Vohra 2/14

8
Eu não acho que você entenda a pergunta. Veja yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.