J’avais annoncé initialement que j’écrirais 3 articles sur le sujet des langages de programmation, et que le troisième serait consacré au thème « Qu’est-ce que j’aimerais avoir comme langage ».
Bon, j’ai menti, il y aura finalement 4 articles, et celui-ci sera consacré à la création d’un langage de programmation interprété.
Je ne vais pas m’étendre sur la théorie de la création des compilateurs et des interpréteurs. J’avais lu un énorme bouquin sur le sujet il y a quelques années ; c’était super intéressant, mais j’avoue ne pas en avoir retenu grand chose de concret. Il faut dire que c’est assez éloigné de mes préoccupations quotidiennes.
Non, je vais plutôt vous parler… d’un moyen de contourner un peu tout ça.
Compilateurs et interpréteurs
Reprenons. Si on simplifie à l’extrême, le travail d’un compilateur c’est ça :
- Lecture du code source.
- Analyses diverses.
- Construction de l’arbre de compilation.
- Optimisation.
- Génération du code machine et écriture du fichier exécutable.
Le boulot d’un interpréteur, c’est plutôt :
- Lecture du code source.
- Analyses diverses.
- Construction de l’arbre d’interprétation.
- Optimisations éventuelles.
- Exécution (évaluation de l’arbre).
Dans le principe, il y a beaucoup de similitudes. D’ailleurs, toutes les explications données dans le livre dont je vous parlais plus haut étaient dans l’optique de construire un interpréteur ; alors que le livre s’intitulait Les compilateurs.
De plus en plus, les interpréteurs passent par un code machine intermédiaire, le bytecode, destiné à être exécuté par une machine virtuelle. Ce code peut parfois être stocké sur disque pour accélérer les exécutions futures, mais ce n’est pas systématique. Cela donne donc des interpréteurs qui fonctionnent ainsi :
- Génération du bytecode
- Lecture du code source.
- Analyses diverses.
- Construction de l’arbre de compilation.
- Optimisation.
- Génération du bytecode.
- Exécution
- Chargement de la machine virtuelle.
- Exécution (évaluation du bytecode par la machine virtuelle).
Ce fonctionnement est censé être plus rapide, car le bytecode est généré spécifiquement pour la machine virtuelle ; celle-ci l’exécute comme du code natif à ses yeux, et non pas comme une évaluation pas-à-pas de l’arbre syntaxique (oh les explications foireuses ! je m’en excuse platement).
Il existe un autre type d’interpréteur, les compilateurs JIT. Leur principe est de compiler à la volée le code source en binaire natif. Le gros intérêt est que le binaire est exécuté directement par l’ordinateur, il n’a pas besoin de machine virtuelle. Pourquoi tous les interpréteurs ne sont pas des compilateurs JIT ? Parce que c’est loin d’être simple à mettre en œuvre. Les compilateurs natifs sont habituellement assez lents ; cela ne pose pas le moindre problème, car quand on compile du code C ou C++, le but est d’obtenir un programme dont l’exécution sera la plus rapide possible ; ils intègrent d’ailleurs plein d’optimisations qui ralentissent la compilation mais qui accélèrent l’exécution.
Un compilateur JIT doit donc être capable de compiler nativement dans un délai très court, afin que l’exécution d’un programme interprété ne semble pas systématiquement ralentie.
Comment créer un interpréteur ?
J’ai été amené à créer plusieurs interpréteurs. Un interpréteur Lisp minimal, un interpréteur de script compatible Bash, un évaluateur mathématique assez poussé dans un outil de génération de PDF. Et je confirme : sans être impossible, ce n’est quand même pas simple.
Et encore, il ne s’agissait que de simples interpréteurs. Pas de bytecode ni de machine virtuelle, et encore moins de compilation native. J’ai bien codé une machine virtuelle particulièrement sommaire durant mes études, mais elle émulait un ordinateur tellement simple que ça ne serait d’aucune utilité.
Par contre, j’ai suivi des cours de compilation, durant lesquels j’ai appris à utiliser Lex et Yacc, qui forment un compilateur de compilateur. Ils offrent les outils de base nécessaires à la réalisation d’un interpréteur ou d’un compilateur (analyse lexicale, analyse syntaxique) : ils permettent de lire un fichier et de le « comprendre » ; à partir de là, on peut soit exécuter du code (interprétation directe), soit construire un arbre d’exécution (pour ensuite interpréter ou compiler l’arbre).
Je dois bien avouer que je n’ai pas utilisé ces programmes une seule fois de toute ma carrière. Ils ne sont pas particulièrement simples à appréhender, mais rien de fondamentalement impossible.
La grande difficulté n’est pas spécialement d’utiliser Lex/Yacc pour extraire les lexèmes qui composent un code source. La grande difficulté, c’est de construire un arbre efficace et de le traiter correctement. C’est la différence entre un petit interpréteur rigolo et un vrai langage.
La solution ?
J’en viens enfin à ma recette miracle. Il existe un compilateur C qui présente trois caractéristiques très intéressantes :
- Il est particulièrement rapide. Il applique forcément moins de d’optimisations sur le code qu’un compilateur comme GCC, mais n’en produit pas moins du code binaire natif très efficace.
- Il peut compiler et exécuter à la volée. Conjugué au point précédent, cela veut dire qu’on peut s’en servir comme d’un interpréteur C. Mais, contrairement aux quelques rares interpréteurs qui utilisent une syntaxe proche du C, le code n’est pas interprété ; il est compilé nativement − très très vite − puis exécuté, et tout ça sans écrire de fichier sur le disque.
- Il est disponible sous la forme d’une bibliothèque de fonctions, elle-même écrite en C, qui permet d’exécuter du code à la volée. Cette bibliothèque permet même de partager des symboles ; ainsi, le programme qui l’utilise pour compiler et exécuter du code à la volée pourra accéder directement aux données et aux fonctions de ce code, et inversement !
Ce compilateur, c’est TinyCC. Il est écrit par le français Fabrice Bellard, qui a aussi à son actif l’émulateur QEMU ou encore la bibliothèque FFmpeg.
Je suis en train de réaliser divers benchmarks en ce moment, pour comparer plusieurs langages de programmation. Je publierai les résultats sur ce blog quand ce sera terminé. Mais je peux déjà vous dire que, pour un code orienté objet avec de l’héritage et du polymorphisme, beaucoup d’instanciations d’objets, des parcours de tableaux et quelques calculs mathématiques, j’obtiens pour le moment les résultats suivants :
- code C compilé avec GCC : 24 ms
- code C++ compilé avec G++ : 25 ms
- code C compilé à la volée avec TinyCC : 150 ms
- code PHP : 2934 ms
Bon, en soit ça ne veut pas dire grand chose (je publierai les codes sources en même temps que l’ensemble des résultats du benchmark). Mais on se rend quand même compte que TinyCC est très efficace (pour info, la compilation avec GCC prend environ 60 ms).
L’idée que j’ai en tête, c’est que plutôt que d’écrire un interpréteur complet (travail de titan), il est certainement plus simple d’écrire un programme qui traduit une syntaxe donnée en code C, que TinyCC peut compiler et exécuter à la volée. En plus, cela offre la possibilité de compiler le code nativement.
D’une certaine manière cela veut dire que le code C devient le bytecode, le langage intermédiaire qui est utilisé pour produire le binaire final.
Je sais que ça peu sembler tordu, mais je suis très humble quant à ma capacité à générer du bytecode et à créer une machine virtuelle. Par contre, je pense pouvoir être capable de créer un traducteur qui génère du code C à partir d’un autre code source.
La vraie question, c’est de savoir si cette étape de traduction ne risque pas à elle seule de plomber les performances d’un tel interpréteur…
Edit : Je découvre a posteriori que cette idée n’est pas si nouvelle que ça. Le projet SmartEiffel (un compilateur open-source du langage Eiffel) effectue la traduction du code Eiffel en code C (ou en bytecode Java), pour ensuite effectuer une compilation native grâce à GCC.
Faisons un test !
Allez, soyons fou, essayons d’écrire un traducteur ultra-simpliste.
Créons un langage de programmation dont la syntaxe est très simpliste. Il n’accepte que 3 types de commandes. Voici un exemple de code source (bon, ça fait un peu pompeux d’appeler ça du code source, mais on est là pour s’amuser) :
# affecte une variable # leurs noms commencent par un dollar suivi d'une seule lettre AFF $a = 3 # incrémente une variable # si elle n'avait pas été affectée, elle est initialisée à 0 INC $b # affichage conditionnel # écrit le texte après les deux-points si la condition est vraie IF $a <= 3 : Texte
Nous allons maintenant écrire un programme qui lit un fichier de code source (passez-moi l’expression), génère du code C dans une variable, puis utilise la bibliothèque libtcc pour compiler et exécuter ce code.
Le début du programme est facile. Quelques inclusions basiques, la définition des prototypes de deux fonctions utilitaires et la fonction principale du programme. Celle-ci alloue une zone mémoire qui contiendra le code C, puis lance la lecture du fichier source et la génération du code C, ensuite elle affiche le contenu de code C généré, et enfin elle appelle la fonction qui exécute le code C.
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <libtcc.h> // prototypes void execute(char*); void readfile(char*, char*); /* fonction principale */ void main(int argc, char **argv) { char result[1024]; // lecture du fichier et génération du code C readfile(argv[1], result); // affichage du code C généré printf("CODE C :\n%s\n", result); // exécution du code C généré execute(result); }
La fonction de lecture et de génération de code initialise quelques variables, ouvre le fichier contenant le code source, lit les instructions qui y sont écrites et ajoute l’équivalent en C dans une chaîne de caractères. Il y a une petite subtilité au sujet des variables : à chaque instruction, on complète un tableau pour indiquer quelles sont les variables qui sont utilisées, afin de faciliter leur déclaration en C.
Edit : J’ai modifié légèrement cette fonction pour gérer les lignes de commentaire (commençant par un caractère dièse). Ce sont les deux lignes en bleu. J’explique plus bas l’utilité réelle de la chose.
/* lit le fichier et génère le code C */ void readfile(char *filename, char *result) { FILE *input; char line[128], buf[128], msg[128], op1, cond[3]; char code[1024], variables[128]; int i; // initialisations result[0] = code[0] = '\0'; strcat(result, "int wrapper() {\n"); for (i = 0; i < 128; ++i) variables[i] = '\0'; // ouverture du fichier source input = fopen(filename, "r"); // lecture de toutes les lignes du fichier while (!feof(input) && fgets(line, 128, input)) { // détection de la commande if (line[0] == '#') continue; if (sscanf(line, "AFF $%c = %d", &op1, &i)) sprintf(buf, "\t%c = %d;\n", op1, i); else if (sscanf(line, "INC $%c", &op1)) sprintf(buf, "\t%c++;\n", op1); else if (sscanf(line, "IF $%c %s %d : %s", &op1, cond, &i, msg)) sprintf(buf, "\tif (%c %s %d) printf(\"%s\\n\");\n", op1, cond, i, msg); variables[op1] = op1; strcat(code, buf); } fclose(input); // définition des variables for (i = 0; i < 128; ++i) { if (variables[i]) { sprintf(buf, "\tint %c = 0;\n", i); strcat(result, buf); } } // ajout du code strcat(result, code); strcat(result, "\treturn 0;\n}\n"); }
Enfin, la fonction d’exécution fait compiler le code C par TinyCC, place le binaire exécutable dans une zone mémoire exploitable, puis appelle la fonction que l’on a créée préalablement.
/* exécute le code généré à la volée */ void execute(char *code) { TCCState *state; int (*wrapper)(void); int size; void *memory; // création du contexte TinyCC state = tcc_new(); tcc_set_output_type(state, TCC_OUTPUT_MEMORY); // compilation du code C tcc_compile_string(state, code); // réallocation en mémoire size = tcc_relocate(state, NULL); memory = malloc(size); tcc_relocate(state, memory); // récupération du pointeur sur la fonction wrapper = tcc_get_symbol(state, "wrapper"); // libération de la mémoire allouée par TinyCC tcc_delete(state); // exécution de la fonction wrapper(); // libération de la mémoire contenant le code binaire free(memory); }
Pour plus d’information sur l’utilisation de libtcc, je ne peux que vous conseiller de lire un très bon article sur le Site du Zéro.
Exécution
Maintenant que nous avons notre super interpréteur, nous allons l’utiliser.
Imaginons que notre code source ressemble à ceci :
AFF $a = 3 AFF $b = 1 IF $a < 5 : ok IF $b > 7 : ko AFF $c = 5 INC $c IF $c >= 6 : parfait
Quand on exécute notre interpréteur, il nous affiche la fonction C générée :
int wrapper() { int a = 0; int b = 0; int c = 0; a = 3; b = 1; if (a < 5) printf("ok\n"); if (b > 7) printf("ko\n"); c = 5; c++; if (c >= 6) printf("parfait\n"); return 0; }
Et donc, le résultat de l’exécution est donc :
ok parfait
Super, ça marche ! Trop fort.
Edit : Pour rendre notre code source directement exécutable (c’est quand même bien utile, quand on fait un interpréteur), il suffit de lui donner les droits d’exécution avec la commande chmod, et d’ajouter le shebang qui conduit vers le programme d’interprétation :
#!/home/amaury/minilang
AFF $a = 3
AFF $b = 1
IF $a < 5 : ok
IF $b > 7 : ko
AFF $c = 5
INC $c
IF $c >= 6 : parfait
Oh miracle ! Le programme s’exécute directement. Même en sachant que ça doit marcher, c’est plaisant à voir.
Par acquis de conscience, je décide de comparer avec le code PHP suivant :
<?php $a = 3; $b = 1; if ($a < 5) print("ok\n"); if ($b > 7) print("ko\n"); $c = 5; $c++; if ($c >= 6) print("parfait\n"); ?>
Et hop, un autre benchmark inutile mais rigolo :
- PHP : 25 ms
- mon interpréteur : 2 ms
Eh oui, c’est débile, ça ne sert à rien, mais ça arrache et ça me fait marrer. Évidemment, plus on ajoutera de vraies fonctionnalités à notre interpréteur pour enrichir sa syntaxe, plus on le ralentira. Mais je pense que ça prouve quand même que mon idée de base est loin d’être idiote : Si vous voulez créer un langage de programmation, utilisez le C comme « langage pivot » ; ce sera plus rapide à mettre en œuvre et plus efficace que si vous développiez votre propre bytecode et votre propre machine virtuelle. En plus vous pourrez accéder à toutes les bibliothèques de fonction disponibles en C, ce qui est loin d’être négligeable.
Salut,
je m’interroge sur la réelle pertinence de ce genre de comparaison.
Tant qu’à faire dans les approximations, ça revient à comparer ton interpréteur avec le Zend Engine et à se demander du coup pourquoi l’interprétation du PHP est « si lente » en comparaison. La conclusion suivante serait alors de se demander s’il ne conviendrait pas d’écrire un autre interpréteur pour le PHP pour gagner quelques millisecondes ..? Mais pour quoi faire ?
Le but est, à priori, un gain de performances, ce qui est fort louable. Mais si à la base on code proprement avec un minimum de rigueur et de discipline, en mettant en application quelques bon principes de programmation, et qu’on ne se limite pas à optimiser le code PHP mais qu’on optimise aussi ce qui est généré, à savoir le langage client qui est envoyé en sortie, on devrait obtenir des performances générale tout à fait acceptables.
Je m’interroge également sur les destinations des langages et la manière dont ils sont utilisés : le PHP est exécuté coté serveur et la sortie aboutit à un langage qui est lui-même interprété par un client sur une autre machine : il y a donc plusieurs points qui peuvent plomber les performances à commencer par la vitesse de transfert disponible suivi immédiatement par les performances de l’interpréteur du client pour aboutir à l’affichage final, interpréteur qui pourrait être lui-même remis en question.
Je dis peut-être n’importe quoi bien entendu, mais je reste sur ma question de départ : dans a pratique, quelle est la pertinence de ce comparatif ?
Ah, mais je l’ai dit et redit : ça ne sert à rien, c’est complètement débile, mes benchmarks ne sont absolument pas pertinents et ne prouvent rien. Mais c’est marrant ! 🙂
Sincèrement, je me suis bien amusé à créer mon petit traducteur. Il faut regarder l’aspect théorique de la chose. Concrètement, qui a vraiment besoin de créer un nouveau langage ?
Pour le reste, l’idée n’est pas de dire que le PHP est spécialement lent (encore une fois, je vais publier un benchmark plus réaliste bientôt). Et même encore comme ça, entre mon mini-mini-langage présenté dans cet article et le PHP, il y a tellement de différences qu’il n’y a même pas de compétition.
Si la performance étant si importante, on coderait nos sites web en assembleur… Moi j’ai arrêté de coder des scripts CGI en C depuis bien longtemps. 😉
Si on laisse de côté l’aspect potache de l’exercice de style, la question des performances n’est pas si idiote non plus. En milieu professionnel, le choix des outils est impactant. Parfois, passer d’une plate-forme à une autre permet de réduire les coûts ; si ton code s’exécute 3 fois plus vite, tu auras besoin de 3 fois moins de serveurs. Ce n’est pas si négligeable.
C’est pour cela que Facebook a créé HipHop, ou encore que le jeu Urban Rivals calcule les coups de son IA en Python plutôt qu’en PHP.
Donc oui, tout cela ne sert à rien, à part partager avec vous mes délires techniques (eh, on est des geeks !). Mais il y a un petit fond de sérieux en toute chose.
Pour rester sur l’aspect théorique purement.
Il me semble que tu as oublié, ou tu l’as fait en tout état de cause, que la grosse différence entre un langage interprété et un langage compilé en dehors des aspects pratiques, c’est la détermination du typage des expressions pendant la phase d’analyse qui aboutie aux performances d’un langage comme le C.
Même avec un Trace Compiler, la compilation d’une trace dois être réeffectuée pour chaque type existant ce qui n’est pas toujours rentable. C’est ce qui fait que les langages à typage dynamique resterons des langages interprétés et plus ou moins interessant à JIT.
Je reviens sur le premier commentaire, au sujet de PHP, oui il mériterait un interpréteur bien plus performant que le zend engine. PHP reste un langage interprété très lent (surtout quand on voit les performance de Lua) et c’est en partie du à une représentation intermédiaire pas vraiment belle (et pas normalisée).
@Seza : Effectivement, le typage dynamique a obligatoirement un coût en performance. Je l’ai mis sciemment de côté pour faciliter les explications.
C’est pareil pour les méthodes virtuelles. À ce sujet, on peut remarquer qu’en PHP et et Java, les méthodes sont virtuelles par défaut (on peut les surcharger tant qu’elles n’ont pas été déclarée final), alors qu’en C++ elles sont statiques par défaut (on ne peut pas les surcharger sauf si elles sont déclarées virtual). Cela facilite grandement le travail du compilateur C++, alors qu’un compilateur Java JIT aura plus de mal à optimiser les choses, pour rendre statiques les méthodes qui ne sont pas surchargées.
Les méthodes virtuelles pénalisent les performances parce qu’elles impliquent une indirection (un pointeur de fonction) à chaque appel.
Lua a des performances bluffantes. Mais il y a un revers à la médaille : le langage offre peu de choses, à part les méta-tables. Il y a moyen de les utiliser pour faire de l’objet, mais ça ne fait pas partie des construction natives du langage (un peu comme de faire de l’objet en C).
J’ai modifié légèrement l’article (texte en bleu). J’ai fait en sorte de pouvoir exécuter directement le code source, celui de mon pseudo-langage. Ça ne sert toujours à rien, mais c’est toujours marrant ! 😉
« Concrètement, qui a vraiment besoin de créer un nouveau langage ? »
Je suis très surpris de lire une telle stupidité sur ce blog.
Ou alors, c’est au moins du second degré, ce qui te connaissant un peu ne m’étonnerait pas, à la réflexion.
Si on suit ce « raisonnement », PHP, Ruby, Python, Lua, le C et tout un tas d’autres langage n’existerait pas, et je ne parle même pas de la programmation fonctionnelle ou objet.
Après tout, personne n’a besoin d’autres paradigmes de programmation ;).
Alors, qui, je ne sais pas, mais ce dont je suis sur et certain, c’est que les langages actuels sont plus ou moins bien adapté à différent contexte de mise en œuvre, et qu’un jour, un type dans son garage inventera un autre langage pour se simplifier la vie parce que ceux dont il dispose ne sont pas suffisamment performant à ces yeux, que ce soit fonctionnellement ou techniquement.
Et je ne parle pas de ceux dont c’est le métier d’inventer de nouveau langage ou ceux qui font de la recherche fondamentale dans ce domaine.
Et c’est exactement la même chose pour les bibliothèques ou les logiciels : j’ai par exemple créé atoum parce que SimpleTest et PHPUnit ne me convenait pas.
@mageekguy : Je retrouve toute ta finesse légendaire ! 😉
Pour traduire plus précisément ma pensée :
– Sur l’ensemble des informaticiens de la planète, il y en a très peu qui créent de nouveaux langages. La plupart utilisent les langages existants pour créer des applications.
– Les quelques personnes qui travaillent − très utilement − sur la création de nouveaux langages, j’imagine qu’elles maîtrisent les techniques de création des interpréteurs et des compilateurs. Elles n’attendent pas après ma bidouille de translation vers du code C compilé à la volée.
J’ai la modestie de croire que les visiteurs de mon blog font majoritairement partie du premier groupe de personnes, pas du second. 🙂
Si mon article aide un bidouilleur (le type dans son garage dont tu parles) à créer un premier prototype de son nouveau langage révolutionnaire, j’en serai très heureux. Mais je n’ai pas d’autre ambition que de partager mes expérimentations techniques. Parce que le gars dans son garage, j’espère qu’il passera rapidement à l’étape suivante qui lui permettra de créer un vrai interpréteur.
J’ai fait un nouvel ajout dans l’article (texte en rouge) : Je me suis rendu compte après coup que le compilateur SmartEiffel effectue en fait une traduction du code Eiffel en code C, qui est lui-même compilé ensuite avec GCC. Bon, le code C généré est pratiquement impossible à lire, mais c’est intéressant (et rassurant !) de voir que je ne suis pas le seul à avoir eu cette idée.
Il y a plein de langage qui au cours de leur vie, avant d’avoir un compilateur natif par exemple, ont utiliser une traduction vers le C.
Rassures toi, ce n’est pas nouveau ni absurde comme méthode. La vrai question est: Est-ce utile de le faire ?
Pour un langage interprété la traduction (et compilation) vers le C aboutira en gros à un executable qui contiendra le code et l’interpréteur de manière embarqué ce qui n’est guère optimisant.
@Seza : Très bonne question. J’en parlais récemment avec un ami, qui me faisait remarquer que lorsqu’on utilise GCJ pour compiler du Java en natif, on se retrouve de toute manière à charger des bibliothèques pour la gestion des spécificités de Java (gestion mémoire, etc.). Pareil quand on compile du C++, on charge des bibliothèques supplémentaires (il suffit d’utiliser la commande ldd pour s’en rendre compte), sans compter l’overhead occasionné par les objets.
Je pense qu’on peut voir 3 approches différentes à cela.
1. La première, c’est celle que tu décris, qui aboutit à la génération d’un exécutable qui intègre à la fois le code de l’interpréteur et celui de l’application. On peut se dire que ça n’a pas grand intérêt, mais c’est quand même pratique de pouvoir obtenir un exécutable confiné, sans interpréteur externe.
2. La seconde approche, c’est d’essayer de générer un code C le plus proche possible d’un code C « natif ». Cela peut être assez facile si le langage d’origine est simple (comme mon exemple dans l’article) et qu’il respecte les même fondements que le C, notamment le typage statique. Bref, pour un langage moderne, ce n’est pas évident ; mais si on y arrive, c’est génial car on bénéficie de performances au top. On pourrait voir ça comme un pré-processeur sous stéroïdes.
3. La dernière approche, c’est de vraiment faire un compilateur JIT. Just In Time = Juste à temps = qui compile les choses juste au moment où il en a besoin. Nous aurions donc un interpréteur au sens classique qui prend en charge la gestion des variables dynamiques et du garbage collector (par exemple), et à chaque fois qu’il doit charger un objet il charge le fichier correspondant, le transforme en C, demande à TinyCC de le compiler, et récupère les symboles pour en exécuter le code ; celui-ci pouvant à son tour faire appel aux fonctionnalités offertes par l’interpréteur pour gérer les variables, le garbage collector, etc.
Ces trois approches ont des avantages et des inconvénients. On peut même réduire à seulement 2 approches différentes, si on considère que la première et la troisième sont équivalentes (la première aboutissant à la génération d’un exécutable natif, alors que la troisième effectue le travail à la volée).
Le méga-pré-processeur peut être intéressant. Par exemple, si on fait de l’embarqué et qu’on n’a pas accès à un compilateur C++, mais qu’on veut quand même s’autoriser quelques facilités (objets avec héritage simple, garbage collector, tableaux associatifs avec une syntaxe comme en Perl ou en PHP).
Il y aurait tellement à dire sur ces approches différentes… Tu as beaucoup d’idées 🙂 Le monde de la compilation est extraordinaire et laisse beaucoup de place à l’imagination.
Tu devrais regarder du côté de LLVM, cela t’ouvrirais bien plus les portes que la translation vers le C. (Et ça t’éviteras à investir un temps non négligeable dans l’apprentissage des connaissances nécessaires à la réalisation d’un backend de compilation (chose passionnante à apprendre ceci-dit mais ça se compte en années)).
Oui, des idées… plutôt des intuitions, parce que mes connaissances sur le sujet sont trop limitées pour que je puisse aborder sérieusement le sujet.
Je connais LLVM de nom. Je vais regarder ce qu’il est possible de faire avec les core libraries qu’il propose (toujours par curiosité intellectuelle).
Je dois quand même avouer que la traduction en C est une part importante du délire. J’aime le C, je manque d’occasion de l’utiliser, j’ai joint l’utile à l’agréable. 😉
Tu peux envoyer à LLVM du bytecode (au format LLVM) ou utiliser les interface fournie en C / C++ et indiquer les opérations que tu souhaites faire (charger un registre, lire un registre, pousser un object sur le stack…) pour directement générer ce bytecode via LLVM.
Par la suite tu peux demander à LLVM d’enregistrer ce bytecode (pour faire un cache d’opcode par ex) ou alors de le JIT et l’éxécuter ou encore de le compiler vers la plateforme de ton choix…
En d’autre mot ça mache sacrément le travail et en plus c’est performant. C’est clair que c’est plus proche de la solution pro que de l’amusement mais on peut très bien s’amuser avec aussi.
salut,
super tutoriel et j’aimerai essayé mais
j’ai essayé d’installer tiny CC sur code::Blocks mais je n’arrive pas voici ce qu’il m’affiche
undifined reference to « tcc_new »
undifined reference to « tcc_set_output_type »
undifined reference to « tcc_compile_string »
undifined reference to « tcc_relocate »
undifined reference to « tcc_relocate »
undifined reference to « tcc_get_symbol »
undifined reference to « tcc_delete »
pouvez-vous me dire ce qui ne va pas
PS: J’ai bien inserer libtcc.a
@remi : Hum, je n’ai jamais utilisé l’IDE Code::Blocks.
Qu’est-ce qui cloche ? Est-ce que c’est la compilation ou le linkage ?
Si c’est la compilation, ça peut vouloir dire qu’il ne trouve pas le fichier de header « libtcc.h ». Si c’est le linkage, c’est qu’il n’a pas pris en compte la bibliothèque (libtcc.a ou libtcc.so).
ça a l’air d’être dans le linkage mais je ne sais pas comment faire pour qu’il soit pris en compte. normalement il faut aller dans Project->Build Option->linker settings mais là ça ne marche pas
Tu ne voudrais pas déjà essayer de compiler simplement en ligne de commande ?
comment fait-on ?
Si ton fichier source s’appelle “minilang.c”, et que tu veux produire un exécutable nommé “minilang” :
gcc minilang.c -ldl -ltcc -o minilang
Il faut que la bibliothèque TinyCC soit installée.
Sous Debian/Ubuntu, il suffit de faire :
sudo apt-get install libtcc-dev
Si tu es sous Windows, tu peux aller voir le lien que j’ai donné sur le site du Zéro, il donne des informations supplémentaires.
merci
j’ai une petite question.
dans le fichier ou on a écrit ce petit code source il y a
AFF $a = 3
là on déclare une variable a qui vaut 3
mais quand je regarde le code source en c
il m’affiche
int a =0;
et plus loin
a=3
alors si j’ai bien compris AFF $a =3 ne fait que d’incrémenter a. Est-ce que s’est ça ?
Salut c’est encore moi je me demande comment on peux enrichir l’interpréteur par exemple ajouter else if ? merci d’avance
@remi : L’instruction AFF sert à affecter une valeur à une variable.
Donc «AFF $a = 3» génère l’instruction «a = 3;». Et forcément, on commence par déclarer la variable par «int a = 0;».
Il est évidemment possible d’ajouter l’instruction else. Ça ne servirait pas à grand-chose, mais bon, tout est possible. Tu peux te charger de ce défi si tu le souhaites 😉
Objection votre honneur : tu introduis une notion qui me semble pourtant primordiale en l’occurrence : il n’appartient pas au développeur de l’interpréteur de décider des choix du développeur de l’application. Note bien que je n’y connais rien, mais il me semble bien que le principe fondamental de n’importe quel langage de programmation est qu’on doit essentiellement interpréter des alternatives et des boucles. Or le développeur de l’application doit pouvoir définir des alternatives, ce qui implique au minimum des « if » et des « else », en option des « elseif » comme on a en PHP.
Si on code futé, on peut arriver dans bien des cas à coder en se limitant à des if en ayant au préalable défini certaines valeurs par défaut, mais qui peut affirmer que c’est toujours possible ?
Donc finalement, je trouve que l’exercice (créer un interpréteur) présente au moins un avantage : faire réaliser les difficultés, les implications et la nécessité du sens aigu du détail pour celui qui code l’interpréteur, et à fortiori celui qui le conçoit à la base et en définit les spécifications fonctionnelles et techniques détaillées 😉
PS : Je suggère l’ajout dans le système de commentaires de ce site d’un bouton [Prévisualiser], j’aime bien me relire avant de poster et surtout vérifier que la mise en forme correspond à ce que j’attends lorsque j’insère certaines balises : là, les correctifs ne sont pas possible avant envoi 😉
Oui mais non. Ce pseudo-interpréteur est juste un « proof of concept ». S’il fallait faire un vrai interpréteur, il faudrait s’y prendre un peu différemment (à plus forte raison si on veut faire un langage dynamique supportant la réflexivité). Donc il y a peu d’intérêt à améliorer ce bout de code pour y ajouter les « else ». Ce serait comme de vouloir y ajouter l’orientation objet ; ce n’est pas le propos.
OK merci et comment faire si je voulais créer un vrai interpréteur
@rémi : Tu peux t’acheter le « Dragon book« , c’est un peu LA référence sur le sujet.
Pour se former plus rapidement, tu peux acheter le livre de Marc-André Cournoyer, ou suivre sa formation à distance (8 heures réparties sur 2 jours). Je l’ai suivie, c’est très intéressant.
Ok merci et est ce qui à un article plutôt SVP?
@rémi : Voici une collection de liens. Si tu n’y trouves pas ton bonheur, je ne peux plus rien pour toi 😉
http://stackoverflow.com/questions/1669/learning-to-write-a-compiler
http://gnuu.org/2009/09/18/writing-your-own-toy-compiler/
http://felix.plesoianu.ro/scratch-lang/
http://www.cs.wright.edu/~tkprasad/courses/cs780/cs780.html
http://www.linux-france.org/article/devl/lexyacc/minimanlexyacc.html
salut
@ Amaury
merci beaucoup
Il y a quelqu’un qui a copié votre article à votre insu : http://www.paperblog.fr/5213728/les-langages-de-programmation-8211-partie-3-creer-un-interpreteur-tinycc-inside/
Sinon merci beaucoup pour votre site très intéressant !
@Manu : Non, pas de soucis, PaperBlog est un intégrateur de flux RSS. Tout va bien. 🙂
C’est quoi comme fichier le tout premier code ?
# affecte une variable
# leurs noms commencent par un dollar suivi d’une seule lettre
AFF $a = 3
# incrémente une variable
# si elle n’avait pas été affectée, elle est initialisée à 0
INC $b
# affichage conditionnel
# écrit le texte après les deux-points si la condition est vraie
IF $a <= 3 : Texte
lui (ou alors j'ai rien compris)
@Bethoth : Je ne suis pas sûr de comprendre ta question.
C’est un fichier qui contient du code qui est ensuite exécuté par mon mini-langage de programmation.