Création de langages JVM utilisables : un aperçu
Publié: 2022-03-11Il existe plusieurs raisons possibles pour créer une langue, dont certaines ne sont pas immédiatement évidentes. Je voudrais les présenter avec une approche pour créer un langage pour la machine virtuelle Java (JVM) en réutilisant autant que possible les outils existants. De cette façon, nous réduirons l'effort de développement et fournirons une chaîne d'outils familière à l'utilisateur, facilitant l'adoption de notre nouveau langage de programmation.
Dans cet article, le premier de la série, je présenterai un aperçu de la stratégie et des divers outils impliqués dans la création de notre propre langage de programmation pour la JVM. dans les prochains articles, nous plongerons dans les détails de la mise en œuvre.
Pourquoi créer votre langage JVM ?
Il existe déjà une infinité de langages de programmation. Alors pourquoi s'embêter à en créer un nouveau ? Il y a plusieurs réponses possibles à cela.
Tout d'abord, il existe de nombreux types de langages différents : voulez-vous créer un langage de programmation à usage général (GPL) ou un langage spécifique à un domaine ? Le premier type comprend des langages comme Java ou Scala : des langages destinés à écrire des solutions suffisamment décentes à un large ensemble de problèmes. Les langages spécifiques au domaine (DSL) se concentrent plutôt sur la résolution d'un ensemble spécifique de problèmes. Pensez à HTML ou Latex : vous pourriez dessiner à l'écran ou générer des documents en Java, mais ce serait fastidieux. Avec ces DSL, vous pouvez créer des documents très facilement, mais ils sont limités à ce domaine spécifique.
Alors peut-être existe-t-il un ensemble de problèmes sur lesquels vous travaillez très souvent et pour lesquels il pourrait être judicieux de créer un DSL. Un langage qui vous rendrait très productif tout en résolvant les mêmes types de problèmes encore et encore.
Peut-être souhaitez-vous plutôt créer une GPL parce que vous avez de nouvelles idées, par exemple pour représenter les relations en tant que citoyens de première classe ou représenter le contexte.
Enfin, vous voudrez peut-être créer une nouvelle langue parce que c'est amusant, cool et parce que vous allez apprendre beaucoup au cours du processus.
Le fait est que si vous ciblez la JVM vous pouvez obtenir un langage utilisable avec un effort réduit, c'est parce que :
- Il vous suffit de générer un bytecode et votre code sera disponible sur toutes les plateformes où il y a une JVM
- Vous pourrez tirer parti de toutes les bibliothèques et frameworks existants pour la JVM
Ainsi, le coût de développement d'un langage est considérablement réduit sur la JVM et il pourrait être judicieux de créer de nouveaux langages dans des scénarios qui ne seraient pas rentables en dehors de la JVM.
De quoi avez-vous besoin pour le rendre utilisable ?
Il existe certains outils dont vous avez absolument besoin pour utiliser votre langage - un parseur et un compilateur (ou un interpréteur) font partie de ces outils. Ce n'est pas sufisant. Pour rendre votre langage vraiment utilisable dans la pratique, vous devez fournir de nombreux autres composants de la chaîne d'outils, éventuellement en s'intégrant aux outils existants.
Idéalement, vous souhaitez pouvoir :
- Gérer les références au code compilé pour la JVM à partir d'autres langages
- Modifiez les fichiers source dans votre IDE préféré avec mise en évidence de la syntaxe, identification des erreurs et auto-complétion
- Vous voulez pouvoir compiler des fichiers en utilisant votre système de construction préféré : maven, gradle ou autres
- Vous souhaitez pouvoir écrire des tests et les exécuter dans le cadre de votre solution d'intégration continue
Si vous pouvez le faire, l'adoption de votre langue sera beaucoup plus facile.
Alors, comment pouvons-nous y parvenir? Dans le reste de l'article, nous examinons les différentes pièces dont nous avons besoin pour rendre cela possible.
Analyse et compilation
La première chose que vous devez faire pour transformer vos fichiers source dans un programme est de les analyser, en obtenant une représentation AST (Abstract-Syntax-Tree) des informations contenues dans le code. À ce stade, vous devrez valider le code : y a-t-il des erreurs de syntaxe ? Erreurs sémantiques ? Vous devez tous les trouver et les signaler à l'utilisateur. Si tout se passe bien, vous devez encore résoudre les symboles. Par exemple, "List" fait-il référence à java.util.List ou java.awt.List ? Lorsque vous invoquez une méthode surchargée, laquelle invoquez-vous ? Enfin, vous devez générer un bytecode pour votre programme.
Ainsi, du code source au bytecode compilé, il y a trois phases principales :
- Construire un AST
- Analyser et transformer l'AST
- Produire le bytecode à partir de l'AST
Voyons ces phases en détail.
Construire un AST : l'analyse syntaxique est une sorte de problème résolu. Il existe de nombreux frameworks, mais je vous suggère d'utiliser ANTLR. Il est bien connu, bien entretenu et possède certaines fonctionnalités qui facilitent la spécification des grammaires (il gère moins de règles récursives - vous n'avez pas besoin de comprendre cela, mais soyez reconnaissant qu'il le fasse !).
Analyser et transformer l'AST : l'écriture d'un système de types, la validation et la résolution des symboles pourraient être difficiles et nécessiter beaucoup de travail. Ce sujet à lui seul nécessiterait un article séparé. Pour l'instant, considérez que c'est la partie de votre compilateur sur laquelle vous allez consacrer le plus d'efforts.
Produire le bytecode à partir de l'AST : cette dernière phase n'est finalement pas si difficile. Vous devriez avoir résolu les symboles dans la phase précédente et préparé le terrain de sorte que vous puissiez essentiellement traduire des nœuds uniques de votre AST transformé en une ou quelques instructions de bytecode. Les structures de contrôle pourraient nécessiter un travail supplémentaire car vous allez traduire vos boucles for, commutateurs, ifs et ainsi de suite dans une séquence de sauts conditionnels et inconditionnels (oui, en dessous de votre beau langage, il y aura encore un tas de gotos). Vous devez apprendre comment la JVM fonctionne en interne, mais la mise en œuvre réelle n'est pas si difficile.
Intégration avec d'autres langues
Lorsque vous aurez obtenu la domination mondiale de votre langage, tout le code sera écrit en l'utilisant exclusivement. Cependant, comme étape intermédiaire, votre langage sera probablement utilisé avec d'autres langages JVM. Peut-être que quelqu'un commencera à écrire quelques cours ou de petits modules dans votre langue dans le cadre d'un projet plus vaste. Il est raisonnable d'espérer pouvoir mélanger plusieurs langages JVM. Alors, comment cela affecte-t-il vos outils linguistiques ?
Vous devez envisager deux scénarios différents :
- Votre langue et les autres vivent dans des modules compilés séparément
- Votre langue et les autres vivent dans les mêmes modules et sont compilées ensemble
Dans le premier scénario, votre code n'a besoin que d'utiliser du code compilé écrit dans d'autres langages. Par exemple, certaines dépendances comme Guava ou des modules dans le même projet peuvent être compilées séparément. Ce type d'intégration nécessite deux choses : premièrement, vous devez être capable d'interpréter les fichiers de classe produits par d'autres langages pour y résoudre les symboles et générer le bytecode pour invoquer ces classes. Le deuxième point est spéculaire au premier : d'autres modules peuvent vouloir réutiliser le code écrit dans votre langage après qu'il ait été compilé. Maintenant, normalement, ce n'est pas un problème car Java peut interagir avec la plupart des fichiers de classe. Cependant, vous pouvez toujours réussir à écrire des fichiers de classe valides pour la JVM mais non invocables depuis Java (par exemple parce que vous utilisez des identifiants non valides en Java).

Le deuxième scénario est plus compliqué : supposons que vous ayez une classe A définie en code Java et une classe B écrite dans votre langage. Supposons que les deux classes se réfèrent l'une à l'autre (par exemple, A pourrait étendre B et B pourrait accepter A comme paramètre pour la même méthode). Maintenant, le fait est que le compilateur Java ne peut pas traiter le code dans votre langage, vous devez donc lui fournir un fichier de classe pour la classe B. Cependant, pour compiler la classe B, vous devez insérer des références à la classe A. Donc, ce que vous devez faire est d'avoir une sorte de compilateur Java partiel, qui, étant donné un fichier source Java, est capable de l'interpréter et d'en produire un modèle que vous pouvez utiliser pour compiler votre classe B. Notez que cela nécessite que vous soyez capable d'analyser le code Java (en utilisant quelque chose comme JavaParser) et résoudre des symboles. Si vous ne savez pas par où commencer, jetez un œil à java-symbol-solver.
Outils : Gradle, Maven, Frameworks de test, CI
La bonne nouvelle est que vous pouvez rendre le fait qu'ils utilisent un module écrit dans votre langue totalement transparent pour l'utilisateur en développant un plugin pour gradle ou maven. Vous pouvez demander au système de construction de compiler des fichiers dans votre langage de programmation. L'utilisateur continuera à exécuter mvn compile ou gradle assemble et ne remarquera aucune différence.
La mauvaise nouvelle est que l'écriture de plugins Maven n'est pas facile : la documentation est très pauvre, peu intelligible et majoritairement obsolète ou tout simplement erronée . Oui, cela ne semble pas réconfortant. Je n'ai pas encore écrit de plugins gradle mais cela semble beaucoup plus facile.
Notez que vous devez également tenir compte de la manière dont les tests peuvent être exécutés à l'aide du système de construction. Pour prendre en charge les tests, vous devez penser à un cadre très basique pour les tests unitaires et vous devez l'intégrer au système de construction, de sorte que l'exécution de test maven recherche des tests dans votre langue, les compile et les exécute en rapportant la sortie à l'utilisateur.
Mon conseil est de regarder les exemples disponibles : l'un d'eux est le plugin Maven pour le langage de programmation de Turin.
Une fois que vous l'avez implémenté, tout le monde devrait pouvoir compiler facilement des fichiers source écrits dans votre langue et les utiliser dans des services d'intégration continue comme Travis.
Plug-in IDE
Un plugin pour un IDE sera l'outil le plus visible pour vos utilisateurs et quelque chose qui affectera grandement la perception de votre langage. Un bon plugin peut aider l'utilisateur à apprendre la langue en fournissant une auto-complétion intelligente, des erreurs contextuelles et des refactorisations suggérées.
Désormais, la stratégie la plus courante consiste à choisir un IDE (généralement Eclipse ou IntelliJ IDEA) et à développer un plugin spécifique pour celui-ci. C'est probablement la partie la plus complexe de votre chaîne d'outils. C'est le cas pour plusieurs raisons : tout d'abord vous ne pouvez pas raisonnablement réutiliser le travail que vous passerez à développer votre plugin pour un IDE pour les autres. Votre Eclipse et votre plugin IntelliJ vont être totalement séparés. Le deuxième point est que le développement de plugins IDE n'est pas très courant, il n'y a donc pas beaucoup de documentation et la communauté est petite. Cela signifie que vous devrez passer beaucoup de temps à comprendre les choses par vous-même. J'ai personnellement développé des plugins pour Eclipse et pour IntelliJ IDEA. Mes questions sur les forums Eclipse sont restées sans réponse pendant des mois ou des années. Sur les forums IntelliJ, j'ai eu plus de chance, et parfois j'ai eu une réponse des développeurs. Cependant, la base d'utilisateurs des développeurs de plugins est plus petite et les API sont très byzantines. Préparez-vous à souffrir.
Il existe une alternative à tout cela, et c'est d'utiliser Xtext. Xtext est un framework pour développer des plugins pour Eclipse, IntelliJ IDEA et le web. Il est né sur Eclipse et vient d'être étendu pour prendre en charge les autres plates-formes. Il n'y a donc pas beaucoup d'expérience à ce sujet, mais cela pourrait être une alternative digne d'être envisagée. Soyons clairs : la seule façon de développer un très bon plugin est de le développer en utilisant l'API native de chaque IDE. Cependant, avec Xtext, vous pouvez obtenir quelque chose de raisonnablement décent avec une fraction de l'effort - il vous suffit de le donner à la syntaxe de votre langage et vous obtenez gratuitement des erreurs de syntaxe/complétion. Pourtant, vous devez implémenter la résolution des symboles et les parties difficiles, mais c'est un point de départ très intéressant ; cependant, les points durs sont l'intégration avec les bibliothèques spécifiques à la plate-forme pour résoudre les symboles Java, donc cela ne résoudra pas vraiment tous vos problèmes.
conclusion
Il existe de nombreuses façons de perdre des utilisateurs potentiels qui ont montré un intérêt pour votre langue. Adopter une nouvelle langue est un défi car cela nécessite de l'apprendre et d'adapter nos habitudes de développement. En réduisant autant que possible l'attrition et en tirant parti de l'écosystème déjà connu de vos utilisateurs, vous pouvez empêcher les utilisateurs d'abandonner avant d'apprendre et de tomber amoureux de votre langue.
Dans le scénario idéal, votre utilisateur pourrait cloner un projet simple écrit dans votre langue et le construire à l'aide des outils standard (Maven ou Gradle) sans remarquer aucune différence. S'il souhaite modifier le projet, il peut l'ouvrir dans son éditeur préféré et le plug-in l'aidera à lui signaler les erreurs et à fournir des complétions intelligentes. Il s'agit d'un scénario très différent de celui d'avoir à comprendre comment invoquer votre compilateur et modifier des fichiers à l'aide du bloc-notes. L'écosystème autour de votre langue peut vraiment faire la différence, et de nos jours, il peut être construit avec un effort raisonnable.
Mon conseil est d'être créatif dans votre langue, mais pas dans vos outils. Réduisez les difficultés initiales auxquelles les gens doivent faire face pour adopter votre langue en utilisant des normes familières.
Bonne conception de langage !
Lectures complémentaires sur le blog Toptal Engineering :
- Comment aborder la rédaction d'un interprète à partir de zéro