web-dev-qa-db-fra.com

Quand est-il judicieux de compiler mon propre langage en code C en premier?

Lors de la conception d'un 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 comme gcc pour finir avec du code machine? Y a-t-il des projets qui utilisent cette approche?

38
danijar

La traduction en code C est une habitude très bien établie. Le C d'origine avec les classes (et les premières implémentations C++, alors appelées Cfront ) l'a fait avec succès. Plusieurs implémentations de LISP ou Scheme le font, par exemple Schicken Cheme , Scheme48 , Bigloo . Certaines personnes ont traduit Prolog en C . De même que certaines versions de Mozart (et il y a eu des tentatives de compilation bytecode Ocaml en C ). L'intelligence artificielle de J.Pitrat système CAIA est également amorcée 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 lors de la traduction en C est appels récursifs de queue . Le standard C ne garantit pas qu'un compilateur C les traduit correctement (en un "saut avec arguments", c'est-à-dire sans manger la pile d'appels), même si dans certains cas, les versions récentes de GCC (ou de Clang/LLVM) font cette optimisation.

Un autre problème est garbage collection . Plusieurs implémentations utilisent simplement Boehm conservative garbage collector (qui est C friendly ...). Si vous vouliez garbage collecter du code (comme le font plusieurs implémentations LISP, par exemple SBCL), cela pourrait être un cauchemar (vous voudriez dlclose sur Posix).

Encore un autre problème concerne la première classe continuations et call/cc . Mais des astuces intelligentes sont possibles (regardez à l'intérieur de Chicken Scheme). Accéder à la pile d'appels pourrait nécessiter de nombreuses astuces (mais voir GNU backtrace , etc ....). Orthogonale persistance de continuations (c'est-à-dire de piles ou de fils) serait difficile en C.

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

Vous voudrez peut-être générer (dans votre code C émis) des directives #line Appropriées. C'est ennuyeux et demande beaucoup de travail (vous voudrez par exemple que cela produise plus facilement gdb - code débogable).

Mon obsolète GCC MELT langage spécifique au domaine lispy (pour personnaliser ou étendre GCC ) est traduit en C (en fait en pauvre C++ maintenant) . Il possède son propre ramasse-miettes de copie générationnel. (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 la machine que dans le code C manuscrit (car vous personnaliserez votre générateur de code C pour votre barrière d'écriture et votre machine GC).

Je ne connais aucune implémentation de langage se traduisant en code C++ authentique , c'est-à-dire en utilisant une technique de "collecte des ordures au moment de la compilation" pour émettre du code C++ en utilisant beaucoup des modèles STL et en respectant l'idiome RAII . (veuillez dire si vous en connaissez un).

Ce qui est drôle aujourd'hui, c'est que (sur les bureaux Linux actuels) les compilateurs C peuvent être assez rapides pour implémenter un niveau supérieur interactif read-eval-print-loop traduit en C: vous allez émettre du code C (un quelques centaines de lignes) à chaque interaction avec l'utilisateur, vous en aurez fork une compilation dans un objet partagé, que vous feriez alors dlopen. (MELT fait tout cela prêt, et il est généralement assez rapide). Tout cela peut prendre quelques dixièmes de seconde et être acceptable par les utilisateurs finaux.

Lorsque cela est possible, je recommanderais de traduire en C, pas en C++, en particulier parce que la compilation C++ est lente. Cependant, C++ a aujourd'hui un puissant standard conteneurs , exceptions , λ-expressions , etc .... et est utilisé ou requis par d'intéressantes bibliothèques C++ ou des cadres tels que Qt , POCO , Tensorflow , et toutes ces fonctionnalités sont ce qui motive le choix de générer du code C++ dans un de mes projets familiers appelé RefPerSys . Si vous générez C++ dynamiquement, acceptez d’attendre plus d’une seconde pour compiler chaque fichier C++ généré (par exemple dans un plugin temporaire, voir pour Linux le mini howto C++ dlopen ) ou utilisez des astuces intelligentes (par exemple ccache et/ou en-têtes précompilés GCC , etc ....) tout en minimisant si possible le montant total de #include - d matériel) pour diminuer le temps de compilation C++.

Si vous implémentez votre langage, vous pouvez également envisager (au lieu d'émettre du code C) des bibliothèques JIT comme libjit , GNU foudre , asmjit , ou même LLVM ou GCCJIT . Si vous voulez traduire en C, vous pourriez parfois utiliser tinycc : il compile très rapidement le code C généré (même en mémoire) à code machine lent . Mais en général, vous voulez profiter des optimisations effectués par un vrai compilateur C comme GCC

Si vous traduisez en C votre langue, assurez-vous de créer le AST du code C généré en mémoire en premier (cela facilite également la génération de tous les déclarations, puis toutes les définitions et le code de fonction). Vous seriez en mesure de faire quelques optimisations/normalisations de cette façon. En outre, vous pourriez être intéressé par plusieurs extensions GCC (par exemple, gotos calculés). Vous voudrez probablement éviter de générer d'énormes fonctions C - par exemple d'une centaine de milliers de lignes de C générées vous feriez mieux de les diviser en plus petits morceaux) car l'optimisation des compilateurs C est très mécontente des très grandes fonctions C (en pratique et expérimentalement, gcc -O temps de compilation des grandes fonctions est proportionnelle 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 --- (Clang (à travers LLVM ) et GCC = (thru libgccjit ) Les compilateurs C & C++ 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 voudrez probablement avoir plusieurs astuces (ou constructions) pour générer un mélange de C avec votre langage. Mon article DSL2011 MELT: un langage spécifique de domaine traduit intégré dans le compilateur GCC devrait vous donner des conseils utiles.

55

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

Typiquement, les langages spécifiques au domaine sont é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é en un exécutable ou une DLL. Le temps nécessaire pour produire un assemblage fonctionnel/bon est beaucoup plus long que la génération de C, et C est assez proche du code d'assemblage pour les performances, il est donc logique de générer 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 aussi d'optimiser - 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 travail acharné.

Il pourrait être plus acceptable de réutiliser le backend du compilateur de LLVM, qui est IIRC neutre en langage, donc vous générez des instructions LLVM au lieu du code C.

8
gbjbaanb

Écrire un compilateur pour produire du code machine peut ne pas être beaucoup plus difficile que d'écrire un qui produit 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 être capable de produire un programme pour n'importe quelle plate-forme qui utilise 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 qui est complètement portable et qui se comportera comme vous le souhaitez sans utiliser de comportements non garantis par la norme C, mais le code qui repose sur des comportements garantis par la plate-forme peut s'exécuter beaucoup plus rapidement sur des plateformes qui font ces garanties que du code qui ne le fait pas.

Par exemple, supposons qu'un langage prend en charge une fonctionnalité pour générer un UInt32 À partir de quatre octets consécutifs d'un UInt8[] Aligné arbitrairement, interprété de façon big-endian. Sur certains compilateurs, on pourrait écrire le code comme:

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

et faire en sorte que le compilateur génère une opération de chargement de mot suivie d'une instruction inverse octets dans Word. Cependant, certains compilateurs ne prendraient pas en charge le modificateur __packed et, en son absence, généreraient du code qui ne fonctionnerait pas.

Alternativement, on pourrait écrire le code comme:

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 celles où CHAR_BITS n'est pas 8 (en supposant que chaque octet de données source se retrouve dans un élément de tableau distinct), mais un tel code peut ne pas fonctionner presque aussi vite que la serait la version non portable sur les plateformes supportant la première.

Notez que la portabilité nécessite souvent que le code soit extrêmement libéral avec des typecasts et des constructions similaires. Par exemple, le code qui veut multiplier deux entiers non signés 32 bits et produire les 32 bits inférieurs du résultat doit pour la portabilité être écrit comme:

uint32_t result = 1u*x*y;

Sans cela 1u, Un compilateur sur un système où INT_BITS allait de 33 à 64 pourrait 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 à profiter de ces opportunités .

2
supercat

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 à des fins d'apprentissage principalement," I ' 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 pouvez utiliser un compilateur existant comme gcc pour vous retrouver avec du code machine, si vous êtes plus intéressé à en apprendre davantage sur le lexical, la syntaxe et l'analyse sémantique que vous en apprenez sur la génération et l'optimisation de code!

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

Si, toutefois, vous êtes dans le programme d'assemblage et fasciné par les défis de l'optimisation du code au niveau le plus bas, alors par tous les moyens, écrivez vous-même un générateur de code pour l'expérience d'apprentissage!

1
Carson63000