Quand est-il judicieux de compiler d'abord mon propre langage au code C?

35

Quand on conçoit son propre langage de programmation, quand est-il judicieux d'écrire un convertisseur qui prend le code source et le convertit en code C ou C ++ afin que je puisse utiliser un compilateur existant tel que gcc pour obtenir du code machine? Y a-t-il des projets qui utilisent cette approche?

Danijar
la source
4
Si vous regardez au-delà de C, vous verrez que C # et Java sont également compilés dans des langages intermédiaires. Vous éviterez ainsi de refaire une partie du travail déjà effectué par quelqu'un d'autre en ciblant un langage intermédiaire au lieu de passer directement à l'assemblage.
Casey,
1
@emodendroket Cependant, C # et Java se compilent en un IL conçu pour être un IL en général et pour C # / Java en particulier, si bien que les bytecodes CIL et JVM sont plus sensés et pratiques qu'un IL ne pourrait jamais l'être. Il ne s'agit pas d'utiliser un langage intermédiaire, mais de choisir quel langage intermédiaire utiliser.
1
Regardez plusieurs implémentations de logiciels libres générant du code C. Et j'espère que votre logiciel d'implémentation linguistique sera gratuit.
Basile Starynkevitch
2
Voici le lien mis à jour à partir du commentaire de @ RobertHarvey: yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean

Réponses:

52

Traduire en code C est une habitude bien établie. Le C d'origine avec les classes (et les premières implémentations de C ++, appelées alors Cfront ) l'ont fait avec succès. Plusieurs implémentations de Lisp ou de Scheme le font, par exemple Chicken Scheme , Scheme48 , Bigloo . Certaines personnes ont traduit Prolog à C . Il en a été de même pour certaines versions de Mozart (et il y a eu plusieurs tentatives pour compiler le bytecode Ocaml en C ). Le système CAIA d' intelligence artificielle de J.Pitrat est également amorcé et génère tout son code C. Vala se traduit également en C, pour le code lié à GTK. Le livre de Queinnec Lisp In Small Pieces avoir un chapitre sur la traduction en C.

L'un des problèmes rencontrés lors de la traduction en C concerne les appels en queue . La norme C ne garantit pas qu'un compilateur C les traduit correctement (en un "saut avec arguments", c'est-à-dire sans manger de pile d'appels), même si dans certains cas, les versions récentes de GCC (ou de Clang / LLVM) effectuent cette optimisation. .

Un autre problème est le ramassage des ordures . Plusieurs implémentations utilisent simplement le récupérateur de déchets conservateur Boehm (qui respecte le C ...). Si vous vouliez récupérer du code (comme le font plusieurs implémentations de Lisp, par exemple SBCL), cela pourrait être un cauchemar (vous voudriez dlclosesur Posix).

Un autre problème concerne les poursuites de première classe et call / cc . Mais des astuces intelligentes sont possibles (regardez à l'intérieur du schéma de poulet). Accéder à la pile d'appels peut nécessiter de nombreuses astuces (mais voir la trace GNU , etc ...). La persistance orthogonale des suites (c.-à-d. Des piles ou des fils) serait difficile chez C.

La gestion des exceptions est souvent une question d'émettre des appels intelligents à longjmp, etc.

Vous voudrez peut-être générer (dans votre code C émis) les #linedirectives appropriées . C'est ennuyeux et demande beaucoup de travail (vous voudrez par exemple produire plus facilement gdbdu code non débugable).

Mon langage spécifique au domaine lispy de MELT (pour personnaliser ou étendre GCC ) est traduit en C (actuellement en C ++ médiocre). Il possède son propre collecteur de déchets de copie générationnelle. (Vous pourriez être intéressé par Qish ou Ravenbrook MPS ). En fait, le GC générationnel est plus facile dans le code C généré par machine que dans le code C écrit à la main (car vous allez adapter votre générateur de code C à votre barrière en écriture et à vos machines GC).

Je ne connais aucune implémentation de langage traduisant en code C ++ authentique , c'est-à-dire utilisant une technique de "récupération de place compile" pour émettre du code C ++ en utilisant de nombreux modèles STL et en respectant l' idiome RAII . (s'il vous plaît dites si vous en connaissez un).

Ce qui est amusant aujourd’hui, c’est que (sur les bureaux Linux actuels), les compilateurs C sont assez rapides pour implémenter une boucle interactive read-eval-print- level de niveau supérieur traduite en C: vous allez émettre du code C (quelques centaines de lignes) pour chaque utilisateur. interaction, vous en aurez forkune compilation dans un objet partagé, que vous feriez ensuite dlopen. (MELT le fait tout prêt, et il est généralement assez rapide). Tout cela pourrait prendre quelques dixièmes de seconde et être acceptable pour les utilisateurs finaux.

Lorsque cela est possible, je recommanderais de traduire en C, pas en C ++, en particulier parce que la compilation en C ++ est lente.

Si vous implémentez votre langage, vous pouvez également envisager (au lieu d'émettre du code C) certaines bibliothèques JIT telles que libjit , GNU lightning , asmjit ou même LLVM ou GCCJIT . Si vous souhaitez traduire en C, vous pouvez parfois utiliser tinycc : il compile très rapidement le code C généré (même en mémoire) pour ralentir le code machine. Mais en général, vous souhaitez tirer parti des optimisations effectuées par un vrai compilateur C tel que GCC.

Si vous traduisez en C votre langue, assurez-vous de commencer par créer l'ensemble de l' AST du code C généré en mémoire (cela facilite également la génération de toutes les déclarations, puis de toutes les définitions et du code de fonction). Vous seriez capable de faire des optimisations / normalisations de cette façon. En outre, vous pourriez être intéressé par plusieurs extensions GCC (par exemple, les gotos calculés). Vous voudrez probablement éviter de générer des fonctions C énormes - par exemple, une ligne de C générée de plusieurs centaines de milliers - (vous ferez mieux de les scinder en morceaux plus petits), car l'optimisation des compilateurs C est très mécontente des très grandes fonctions C (en pratique, et expérimentalement,gcc -Ole temps de compilation de grandes fonctions est proportionnel au carré de la taille du code de fonction). Limitez donc la taille de vos fonctions C générées à quelques milliers de lignes chacune.

Notez que les compilateurs C & C ++ de Clang (via LLVM ) et GCC (via libgccjit ) offrent un moyen d'émettre des représentations internes adaptées à ces compilateurs, mais cela pourrait (ou non) être plus difficile que d'émettre du code C (ou C ++), et est spécifique à chaque compilateur.

Si vous concevez un langage à traduire en C, vous souhaiterez probablement disposer de plusieurs astuces (ou constructions) pour générer un mélange de C avec votre langage. Mon document DSL2011 MELT: un langage spécifique à un domaine traduit intégré au compilateur GCC devrait vous donner des conseils utiles.

Basile Starynkevitch
la source
Faites-vous allusion à "programme de poulet?"
Robert Harvey
1
Oui. J'ai donné l'URL.
Basile Starynkevitch
Est-il relativement pratique de faire en sorte qu'une machine virtuelle, telle que Java ou autre, compile du code binaire en C, puis utilise gcc pour la compilation JIT? Ou devraient-ils simplement passer directement du code binaire à l’assemblage?
Panzercrisis
1
@Panzercrisis La plupart des compilateurs JIT ont besoin de leur moteur de code machine pour prendre en charge des tâches telles que le remplacement d'une fonction et le correctif du code existant avec une trappe d'accès. En dehors de cela, gcc est spécifiquement ... architecturalement moins adapté à la compilation JIT et à d'autres cas d'utilisation. Découvrez cependant libgccjit: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html et gcc.gnu.org/wiki/JIT
1
Excellent matériau d'orientation. Merci!
8

Il est logique que le temps nécessaire pour générer le code machine complet l'emporte sur l'inconvénient d'une étape intermédiaire de compilation de votre "IL" en code machine à l'aide d'un compilateur C.

Les langages spécifiques à un domaine sont généralement écrits de cette manière. Un système de très haut niveau est utilisé pour définir ou décrire un processus qui est ensuite compilé dans un exécutable ou une dll. Le temps nécessaire pour produire un assemblage qui fonctionne bien est beaucoup plus long que pour générer du C, et C est un code d'assemblage assez proche de la performance. Il est donc logique de générer du C et de réutiliser les compétences des rédacteurs du compilateur C. Notez qu’il ne s’agit pas seulement de compiler, mais d’optimiser aussi - les gars qui écrivent gcc ou llvm ont passé beaucoup de temps à créer du code machine optimisé, il serait idiot d’essayer de réinventer tout leur dur travail.

Il serait peut-être plus acceptable de réutiliser le back-end du compilateur de LLVM dont le code IIRC est indépendant du langage. Vous devez donc générer des instructions LLVM au lieu du code C.

gbjbaanb
la source
On dirait que les bibliothèques sont une raison assez convaincante de l’envisager aussi.
Casey
Quand vous dites "votre 'IL'", de quoi parlez-vous? Un arbre de syntaxe abstrait?
Robert Harvey
@ RobertHarvey Non, je veux dire le code C. Dans le cas des PO, il s’agit d’un langage intermédiaire situé à mi-chemin entre son propre langage évolué et le code machine. Je le mets entre guillemets pour essayer de transmettre l'idée selon laquelle il n'est pas utilisé par de nombreuses personnes (par exemple Microsoft .NET IL par exemple)
gbjbaanb
2

Ecrire un compilateur pour produire du code machine peut ne pas être beaucoup plus difficile que d’écrire un qui produit du C (dans certains cas, cela peut être plus facile), mais un compilateur qui produit du code machine ne pourra produire que des programmes exécutables sur la plate-forme particulière pour laquelle c'était écrit; un compilateur qui produit du code C, en revanche, peut produire un programme pour toute plate-forme utilisant un dialecte de C que le code généré est conçu pour prendre en charge. Notez que dans de nombreux cas, il peut être possible d'écrire du code C complètement portable et qui se comportera comme souhaité sans utiliser aucun comportement non garanti par le standard C, mais un code reposant sur des comportements garantis par la plate-forme pourra s'exécuter beaucoup plus rapidement. sur des plates-formes qui offrent ces garanties que du code qui ne le fait pas.

Par exemple, supposons qu'un langage prenne en charge une fonctionnalité permettant de générer un élément UInt32parmi quatre octets consécutifs d'un fichier arbitrairement aligné UInt8[]et interprété de manière big-endian. Sur certains compilateurs, on pourrait écrire le code comme suit:

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

et demandez au compilateur de générer une opération de chargement de mots suivie d'une instruction d'inverse d'octets dans le mot. Cependant, certains compilateurs ne prendraient pas en charge le modificateur __packed et généreraient du code qui ne fonctionnerait pas.

Alternativement, on pourrait écrire le code comme suit:

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

un tel code devrait fonctionner sur n’importe quelle plate-forme, même sur celles où il CHAR_BITSn’est pas 8 (en supposant que chaque octet de données source se retrouve dans un élément de tableau distinct), mais ce code risque de ne pas fonctionner aussi vite que le ferait le non-portable version sur les plates-formes supportant l’ancienne.

Notez que la portabilité nécessite souvent que le code soit extrêmement libéral avec des transmissions de type et des constructions similaires. Par exemple, le code qui veut multiplier deux entiers non signés de 32 bits et générer les 32 bits les plus bas du résultat doit, pour des raisons de portabilité, être écrit ainsi:

uint32_t result = 1u*x*y;

Sans cela 1u, un compilateur sur un système où INT_BITS allait de 33 à 64 pouvait légitimement faire tout ce qu'il voulait si le produit de x et y était supérieur à 2 147 483 647, et certains compilateurs sont enclins à tirer parti de ces opportunités.

supercat
la source
1

Vous avez d'excellentes réponses ci-dessus, mais étant donné que, dans un commentaire, vous avez répondu à la question "Pourquoi voulez-vous créer votre propre langage de programmation en premier lieu?" Avec "Ce serait principalement à des fins d'apprentissage," Je " Je vais répondre sous un angle différent.

Il est logique d'écrire un convertisseur qui prend le code source et le convertit en code C ou C ++, de sorte que vous puissiez utiliser un compilateur existant tel que gcc pour obtenir du code machine, si vous souhaitez en savoir plus sur le lexical, la syntaxe et analyse sémantique que vous en apprendre sur la génération de code et l'optimisation!

Écrire votre propre générateur de code machine est un travail assez important que vous pouvez éviter en compilant en C, si ce n’est pas ce qui vous intéresse le plus!

Si, toutefois, vous êtes dans le programme d’assemblage et que vous êtes fasciné par l’optimisation du code au niveau le plus bas, écrivez un générateur de code pour l’apprentissage!

Carson63000
la source
-7

Cela dépend du système d'exploitation que vous utilisez si vous utilisez Windows, il existe un Microsoft IL (Intermediate Language) qui convertit votre code en langage intermédiaire de sorte qu'il ne faut pas longtemps pour être compilé en code machine. Ou si vous utilisez Linux, il existe un compilateur séparé pour cela

Pour revenir à votre question, vous devez avoir un compilateur ou un interprète séparé pour la conception de votre propre langage, car la machine ne connaît pas le langage de haut niveau. Votre code doit être compilé en code machine pour le rendre utile pour la machine

Tayyab Gulsher Vohra
la source
2
Your code should be compiled into machine code to make it useful for machine- Si votre compilateur a généré du code c en tant que sortie, vous pouvez l'insérer dans un compilateur CA pour générer du code machine, n'est-ce pas?
Robert Harvey
Oui. parce que la machine n'a pas le langage c
Tayyab Gulsher Vohra
2
Droite. La question était donc: "Quand est-il judicieux d’émettre c et d’utiliser un compilateur ca plutôt que d’émettre directement du langage machine ou du code octet?"
Robert Harvey
en réalité, il demande à concevoir son langage de programmation dans lequel il demande que "le convertisse en code C ou C ++". Donc, j'explique ceci si vous concevez votre propre langage de programmation, pourquoi vous devriez utiliser le compilateur c ou c ++. si vous êtes assez intelligent, vous devriez concevoir le vôtre
Tayyab Gulsher Vohra
8
Je ne pense pas que vous compreniez la question. Voir yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey