Creazione di linguaggi JVM utilizzabili: una panoramica
Pubblicato: 2022-03-11Ci sono diverse possibili ragioni per creare una lingua, alcune delle quali non sono immediatamente ovvie. Vorrei presentarli insieme a un approccio per creare un linguaggio per la Java Virtual Machine (JVM) riutilizzando il più possibile gli strumenti esistenti. In questo modo ridurremo lo sforzo di sviluppo e forniremo una toolchain familiare all'utente, facilitando l'adozione del nostro nuovo linguaggio di programmazione.
In questo articolo, il primo della serie, presenterò una panoramica della strategia e dei vari strumenti coinvolti nella creazione del nostro linguaggio di programmazione per la JVM. nei prossimi articoli ci addentreremo nei dettagli di implementazione.
Perché creare la tua lingua JVM?
Esistono già un numero infinito di linguaggi di programmazione. Allora perché preoccuparsi di crearne uno nuovo? Ci sono molte possibili risposte a questo.
Innanzitutto, ci sono molti tipi diversi di linguaggi: vuoi creare un linguaggio di programmazione generico (GPL) o uno specifico di dominio? Il primo tipo include linguaggi come Java o Scala: linguaggi destinati a scrivere soluzioni sufficientemente decenti per un ampio insieme di problemi. I Domain Specific Languages (DSL) si concentrano invece sulla risoluzione molto bene di una serie specifica di problemi. Pensa a HTML o Latex: potresti disegnare sullo schermo o generare documenti in Java ma sarebbe ingombrante, con questi DSL invece puoi creare documenti molto facilmente ma sono limitati a quel dominio specifico.
Quindi forse c'è un insieme di problemi su cui si lavora molto spesso e per i quali potrebbe avere senso creare una DSL. Un linguaggio che ti renderebbe molto produttivo mentre risolverai sempre lo stesso tipo di problemi.
Forse invece vuoi creare un GPL perché avevi delle nuove idee, ad esempio per rappresentare le relazioni come cittadini di prim'ordine o rappresentare il contesto.
Infine, potresti voler creare una nuova lingua perché è divertente, interessante e perché imparerai molto nel processo.
Il fatto è che se miri alla JVM puoi ottenere un linguaggio utilizzabile con uno sforzo ridotto, questo perché:
- Devi solo generare bytecode e il tuo codice sarà disponibile su tutte le piattaforme in cui è presente una JVM
- Sarai in grado di sfruttare tutte le librerie e i framework esistenti per la JVM
Quindi il costo di sviluppo di una lingua è notevolmente ridotto sulla JVM e potrebbe avere senso creare nuove lingue in scenari che sarebbero antieconomici al di fuori della JVM.
Di cosa hai bisogno per renderlo utilizzabile?
Ci sono alcuni strumenti di cui hai assolutamente bisogno per usare il tuo linguaggio: un parser e un compilatore (o un interprete) sono tra questi strumenti. Comunque, questo non è abbastanza. Per rendere il tuo linguaggio realmente utilizzabile nella pratica è necessario fornire molti altri componenti della catena degli strumenti, integrandoli possibilmente con gli strumenti esistenti.
Idealmente vuoi essere in grado di:
- Gestisci i riferimenti al codice compilato per la JVM da altri linguaggi
- Modifica i file sorgente nel tuo IDE preferito con l'evidenziazione della sintassi, l'identificazione degli errori e il completamento automatico
- Vuoi essere in grado di compilare file usando il tuo sistema di build preferito: maven, gradle o altri
- Vuoi essere in grado di scrivere test ed eseguirli come parte della tua soluzione di integrazione continua
Se puoi farlo, adottare la tua lingua sarà molto più facile.
Quindi, come possiamo raggiungerlo? Nel resto del post esaminiamo i diversi pezzi di cui abbiamo bisogno per renderlo possibile.
Analisi e compilazione
La prima cosa che devi fare per trasformare i tuoi file sorgente in un programma è analizzarli, ottenendo una rappresentazione Abstract-Syntax-Tree (AST) delle informazioni contenute nel codice. A quel punto dovrai validare il codice: ci sono errori sintattici? Errori semantici? Devi trovarli tutti e segnalarli all'utente. Se tutto procede senza intoppi, devi comunque risolvere i simboli. Ad esempio, "List" si riferisce a java.util.List o java.awt.List ? Quando invochi un metodo sovraccarico, quale stai invocando? Infine, devi generare bytecode per il tuo programma.
Quindi, dal codice sorgente al bytecode compilato ci sono tre fasi principali:
- Costruire un AST
- Analizzare e trasformare l'AST
- Produzione del bytecode dall'AST
Vediamo quelle fasi in dettaglio.
Costruire un AST : l'analisi è una sorta di problema risolto. Ci sono molti framework là fuori, ma ti suggerisco di usare ANTLR. È ben noto, ben mantenuto e ha alcune caratteristiche che rendono più facile specificare le grammatiche (gestisce regole meno ricorsive - non è necessario capirlo, ma sii grato che lo faccia!).
Analizzare e trasformare l'AST : scrivere un sistema di tipi, la convalida e la risoluzione dei simboli potrebbe essere impegnativo e richiedere un bel po' di lavoro. Questo argomento da solo richiederebbe un post separato. Per ora considera che questa è la parte del tuo compilatore su cui spenderai la maggior parte dello sforzo.
Produzione del bytecode dall'AST : quest'ultima fase in realtà non è poi così difficile. Dovresti aver risolto i simboli nella fase precedente e preparato il terreno in modo che sostanzialmente tu possa tradurre i singoli nodi del tuo AST trasformato in una o poche istruzioni di bytecode. Le strutture di controllo potrebbero richiedere del lavoro extra perché tradurrai i tuoi loop for, switch, if e così via in una sequenza di salti condizionali e incondizionati (sì, sotto il tuo bel linguaggio ci saranno ancora un sacco di goto). Devi imparare come funziona la JVM internamente, ma l'implementazione effettiva non è così difficile.
Integrazione con altre lingue
Quando avrai ottenuto il dominio del mondo per la tua lingua, tutto il codice verrà scritto utilizzandola esclusivamente. Tuttavia, come passaggio intermedio, la tua lingua verrà probabilmente utilizzata insieme ad altre lingue JVM. Forse qualcuno inizierà a scrivere un paio di classi o piccoli moduli nella tua lingua all'interno di un progetto più ampio. È ragionevole aspettarsi di poter mescolare diversi linguaggi JVM. Quindi, come influisce sui tuoi strumenti linguistici?
Devi considerare due diversi scenari:
- La tua lingua e le altre vivono in moduli compilati separatamente
- La tua lingua e le altre vivono negli stessi moduli e sono compilate insieme
Nel primo scenario il tuo codice deve usare solo codice compilato scritto in altri linguaggi. Ad esempio, alcune dipendenze come Guava o moduli nello stesso progetto possono essere compilate separatamente. Questo tipo di integrazione richiede due cose: in primo luogo, dovresti essere in grado di interpretare i file di classe prodotti da altri linguaggi per risolvere i simboli su di essi e generare il bytecode per invocare quelle classi. Il secondo punto è speculare al primo: altri moduli potrebbero voler riutilizzare il codice scritto nella tua lingua dopo che è stato compilato. Ora, normalmente non è un problema perché Java può interagire con la maggior parte dei file di classe. Tuttavia potresti comunque riuscire a scrivere file di classe che sono validi per la JVM ma non possono essere richiamati da Java (per esempio perché usi identificatori che non sono validi in Java).

Il secondo scenario è più complicato: supponiamo di avere una classe A definita nel codice Java e una classe B scritta nella tua lingua. Supponiamo che le due classi si riferiscano l'una all'altra (per esempio A potrebbe estendere B e B potrebbe accettare A come parametro per lo stesso metodo). Ora il punto è che il compilatore Java non può elaborare il codice nella tua lingua, quindi devi fornirgli un file di classe per la classe B. Tuttavia per compilare la classe B devi inserire riferimenti alla classe A. Quindi quello che devi fare è avere una sorta di compilatore Java parziale, che dato un file sorgente Java è in grado di interpretarlo e produrne un modello che puoi usare per compilare la tua classe B. Nota che questo richiede che tu sia in grado di analizzare il codice Java (usando qualcosa come JavaParser) e risolvere i simboli. Se non hai idea da dove cominciare dai un'occhiata a java-symbol-solver.
Strumenti: Gradle, Maven, Test Framework, CI
La buona notizia è che puoi rendere totalmente trasparente all'utente il fatto che stanno usando un modulo scritto nella tua lingua sviluppando un plugin per gradle o maven. Puoi istruire il sistema di compilazione per compilare i file nel tuo linguaggio di programmazione. L'utente continuerà a eseguire mvn compile o gradle assemble e non noterà alcuna differenza.
La cattiva notizia è che scrivere plugin Maven non è facile: la documentazione è molto scarsa, non comprensibile e perlopiù obsoleta o semplicemente sbagliata . Sì, non suona confortante. Non ho ancora scritto plugin gradle ma sembra molto più semplice.
Tieni presente che dovresti anche considerare come eseguire i test utilizzando il sistema di compilazione. Per supportare i test dovresti pensare a un framework molto semplice per i test unitari e dovresti integrarlo con il sistema di build, in modo che l'esecuzione di maven test cerchi i test nella tua lingua, li compili ed eseguili segnalando l'output all'utente.
Il mio consiglio è di guardare gli esempi disponibili: uno di questi è il plugin Maven per il linguaggio di programmazione Torino.
Una volta implementato, tutti dovrebbero essere in grado di compilare facilmente file sorgente scritti nella tua lingua e utilizzarli nei servizi di integrazione continua come Travis.
Plugin IDE
Un plug-in per un IDE sarà lo strumento più visibile per i tuoi utenti e qualcosa che influenzerà notevolmente la percezione della tua lingua. Un buon plugin può aiutare l'utente ad apprendere la lingua fornendo un completamento automatico intelligente, errori contestuali e refactoring suggeriti.
Ora, la strategia più comune è scegliere un IDE (tipicamente Eclipse o IntelliJ IDEA) e sviluppare un plug-in specifico per esso. Questo è probabilmente il pezzo più complesso della tua toolchain. Questo è il caso per diversi motivi: prima di tutto non puoi riutilizzare ragionevolmente il lavoro che impiegherai a sviluppare il tuo plugin per un IDE per gli altri. Il tuo Eclipse e il tuo plug-in IntelliJ saranno completamente separati. Il secondo punto è che lo sviluppo di plugin IDE non è molto comune, quindi non c'è molta documentazione e la comunità è piccola. Significa che dovrai dedicare molto tempo a capire le cose da solo. Ho sviluppato personalmente plugin per Eclipse e per IntelliJ IDEA. Le mie domande sui forum di Eclipse sono rimaste senza risposta per mesi o anni. Sui forum IntelliJ ho avuto più fortuna ea volte ho ricevuto una risposta dagli sviluppatori. Tuttavia, la base di utenti degli sviluppatori di plugin è più piccola e le API sono molto bizantine. Preparati a soffrire.
C'è un'alternativa a tutto questo, ed è usare Xtext. Xtext è un framework per lo sviluppo di plugin per Eclipse, IntelliJ IDEA e il web. È nato su Eclipse ed è stato recentemente esteso per supportare le altre piattaforme, quindi non c'è tanta esperienza in merito ma potrebbe essere un'alternativa degna di essere considerata. Consentitemi di chiarire: l'unico modo per sviluppare un ottimo plug-in è svilupparlo utilizzando l'API nativa di ogni IDE. Tuttavia, con Xtext puoi avere qualcosa di ragionevolmente decente con una frazione dello sforzo: lo dai semplicemente alla sintassi della tua lingua e ottieni errori/completamento di sintassi gratuitamente. Tuttavia, devi implementare la risoluzione dei simboli e le parti difficili, ma questo è un punto di partenza molto interessante; tuttavia, i bit difficili sono l'integrazione con le librerie specifiche della piattaforma per risolvere i simboli Java, quindi questo non risolverà davvero tutti i tuoi problemi.
Conclusioni
Ci sono molti modi in cui potresti perdere potenziali utenti che hanno mostrato interesse per la tua lingua. L'adozione di una nuova lingua è una sfida perché richiede l'apprendimento e l'adattamento delle nostre abitudini di sviluppo. Riducendo il più possibile l'attrito e sfruttando l'ecosistema già noto ai tuoi utenti, puoi impedire agli utenti di arrendersi prima che imparino e si innamorino della tua lingua.
Nello scenario ideale, il tuo utente potrebbe clonare un semplice progetto scritto nella tua lingua e costruirlo utilizzando gli strumenti standard (Maven o Gradle) senza notare alcuna differenza. Se vuole modificare il progetto, può aprirlo nel suo editor preferito e il plug-in lo aiuterà a segnalargli errori e fornire completamenti intelligenti. Questo è uno scenario molto diverso dal dover capire come richiamare il compilatore e modificare i file usando il blocco note. L'ecosistema attorno alla tua lingua può davvero fare la differenza e oggigiorno può essere costruito con uno sforzo ragionevole.
Il mio consiglio è di essere creativi nella tua lingua, ma non nei tuoi strumenti. Riduci le difficoltà iniziali che le persone devono affrontare per adottare la tua lingua utilizzando standard familiari.
Buona progettazione del linguaggio!
Ulteriori letture sul blog di Toptal Engineering:
- Come avvicinarsi alla scrittura di un interprete da zero