Crearea limbajelor JVM utilizabile: o prezentare generală

Publicat: 2022-03-11

Există mai multe motive posibile pentru crearea unei limbi, dintre care unele nu sunt imediat evidente. Aș dori să le prezint împreună cu o abordare de a face un limbaj pentru Java Virtual Machine (JVM) reutilizand cât mai mult posibil instrumentele existente. În acest fel, vom reduce efortul de dezvoltare și vom oferi un lanț de instrumente familiar utilizatorului, facilitând adoptarea noului nostru limbaj de programare.

Crearea limbajelor JVM utilizabile: o prezentare generală

În acest articol, primul din serie, voi prezenta o privire de ansamblu asupra strategiei și diverselor instrumente implicate în crearea propriului nostru limbaj de programare pentru JVM. în articolele viitoare, ne vom scufunda în detaliile implementării.

De ce să vă creați limbajul JVM?

Există deja un număr infinit de limbaje de programare. Deci, de ce să vă deranjați să creați unul nou? Există multe răspunsuri posibile la asta.

În primul rând, există multe tipuri diferite de limbaje: doriți să creați un limbaj de programare cu scop general (GPL) sau unul specific domeniului? Primul tip include limbaje precum Java sau Scala: limbaje destinate să scrie soluții suficient de decente pentru un set mare de probleme. Limbaje specifice domeniului (DSL) se concentrează în schimb pe rezolvarea foarte bine a unui set specific de probleme. Gândiți-vă la HTML sau Latex: ați putea să desenați pe ecran sau să generați documente în Java, dar ar fi greoi, cu aceste DSL-uri în schimb puteți crea documente foarte ușor, dar sunt limitate la acel domeniu specific.

Deci, poate că există un set de probleme la care lucrați foarte des și pentru care ar putea avea sens să creați un DSL. Un limbaj care te-ar face foarte productiv în timp ce rezolvi aceleași tipuri de probleme din nou și din nou.

Poate, în schimb, doriți să creați o GPL pentru că ați avut câteva idei noi, de exemplu pentru a reprezenta relațiile ca cetățeni de primă clasă sau pentru a reprezenta context.

În cele din urmă, poate doriți să creați o nouă limbă pentru că este distractivă, cool și pentru că veți învăța multe în acest proces.

Faptul este că dacă vizați JVM-ul puteți obține un limbaj utilizabil cu un efort redus, asta pentru că:

  • Trebuie doar să generați bytecode și codul dvs. va fi disponibil pe toate platformele unde există un JVM
  • Veți putea folosi toate bibliotecile și cadrele existente pentru JVM

Deci costul dezvoltării unui limbaj este mult redus pe JVM și ar putea avea sens să se creeze noi limbaje în scenarii care ar fi neeconomice în afara JVM.

De ce ai nevoie pentru a-l face utilizabil?

Există câteva instrumente de care aveți absolut nevoie pentru a vă folosi limbajul - un parser și un compilator (sau un interpret) sunt printre aceste instrumente. Cu toate acestea, acest lucru nu este suficient. Pentru a face limbajul cu adevărat utilizabil în practică, trebuie să furnizați multe alte componente ale lanțului de instrumente, eventual integrarea cu instrumentele existente.

În mod ideal, doriți să puteți:

  • Gestionați referințele la codul compilat pentru JVM din alte limbi
  • Editați fișierele sursă în IDE-ul dvs. preferat cu evidențierea sintaxelor, identificarea erorilor și completarea automată
  • Vrei să poți compila fișiere folosind sistemul tău de construcție preferat: maven, gradle sau altele
  • Doriți să puteți scrie teste și să le rulați ca parte a soluției dvs. de integrare continuă

Dacă poți face asta, îți va fi mult mai ușor să-ți adopti limba.

Deci, cum putem realiza asta? În restul postării examinăm diferitele piese de care avem nevoie pentru a face acest lucru posibil.

Analizare și compilare

Primul lucru pe care trebuie să-l faceți pentru a vă transforma fișierele sursă într-un program este să le analizați, obținând o reprezentare Abstract-Syntax-Tree (AST) a informațiilor conținute în cod. În acel moment va trebui să validați codul: există erori sintactice? Erori semantice? Trebuie să le găsiți pe toate și să le raportați utilizatorului. Dacă totul decurge fără probleme, mai trebuie să rezolvați simbolurile. De exemplu, „List” se referă la java.util.List sau java.awt.List ? Când invoci o metodă supraîncărcată, pe care o invoci? În cele din urmă, trebuie să generați bytecode pentru programul dvs.

Deci, de la codul sursă la bytecode compilat există trei faze principale:

  1. Construirea unui AST
  2. Analizarea și transformarea AST
  3. Producerea bytecode din AST

Să vedem acele faze în detaliu.

Construirea unui AST : analiza este un fel de problemă rezolvată. Există multe cadre, dar vă sugerez să utilizați ANTLR. Este bine cunoscut, bine întreținut și are câteva caracteristici care ușurează specificarea gramaticilor (se ocupă de reguli mai puțin recursive - nu trebuie să înțelegeți asta, dar fiți recunoscători că face!).

Analizarea și transformarea AST : scrierea unui sistem de tip, validarea și rezoluția simbolurilor ar putea fi o provocare și necesită destul de multă muncă. Numai acest subiect ar necesita o postare separată. Deocamdată luați în considerare că aceasta este partea compilatorului pe care veți cheltui cel mai mult efort.

Producerea bytecode din AST : această ultimă fază nu este de fapt atât de dificilă. Ar fi trebuit să rezolvați simbolurile în faza anterioară și să pregătiți terenul astfel încât, practic, să puteți traduce nodurile individuale ale AST transformat în unul sau câteva instrucțiuni de cod de octet. Structurile de control ar putea necesita ceva muncă suplimentară, deoarece vă veți traduce buclele for, comutatoarele, if-urile și așa mai departe într-o succesiune de salturi condiționate și necondiționate (da, sub limbajul vostru frumos vor fi încă o grămadă de gotos). Trebuie să aflați cum funcționează JVM-ul intern, dar implementarea efectivă nu este atât de dificilă.

Integrare cu alte limbi

Când vei fi obținut dominația mondială pentru limba ta, tot codul va fi scris folosindu-l exclusiv. Cu toate acestea, ca pas intermediar, limba dvs. va fi probabil utilizată împreună cu alte limbaje JVM. Poate că cineva va începe să scrie câteva clase sau câteva module mici în limba ta în cadrul unui proiect mai mare. Este rezonabil să ne așteptăm să puteți amesteca mai multe limbaje JVM. Deci, cum vă afectează instrumentele lingvistice?

Trebuie să luați în considerare două scenarii diferite:

  • Limba dvs. și ceilalți trăiesc în module compilate separat
  • Limba dvs. și celelalte locuiesc în aceleași module și sunt compilate împreună

În primul scenariu, codul dvs. trebuie să utilizeze doar cod compilat scris în alte limbi. De exemplu, unele dependențe precum Guava sau module din același proiect pot fi compilate separat. Acest tip de integrare necesită două lucruri: în primul rând, ar trebui să puteți interpreta fișierele de clasă produse de alte limbi pentru a le rezolva simboluri și pentru a genera bytecode pentru invocarea acelor clase. Al doilea punct este specular cu primul: alte module ar putea dori să refolosească codul scris în limba dumneavoastră după ce acesta a fost compilat. Acum, în mod normal, aceasta nu este o problemă, deoarece Java poate interacționa cu majoritatea fișierelor de clasă. Totuși, puteți reuși să scrieți fișiere de clasă care sunt valide pentru JVM, dar nu pot fi invocate din Java (de exemplu, deoarece utilizați identificatori care nu sunt validi în Java).

Al doilea scenariu este mai complicat: să presupunem că aveți o clasă A definită în cod Java și o clasă B scrisă în limba dvs. Să presupunem că cele două clase se referă una la cealaltă (de exemplu, A ar putea extinde B și B ar putea accepta A ca parametru pentru aceeași metodă). Acum, ideea este că compilatorul Java nu poate procesa codul în limba dvs., așa că trebuie să îi furnizați un fișier de clasă pentru clasa B. Cu toate acestea, pentru a compila clasa B, trebuie să inserați referințe la clasa A. Deci, ceea ce trebuie să faceți este să aveți un fel de compilator Java parțial, care, având în vedere un fișier sursă Java, este capabil să îl interpreteze și să producă un model al acestuia pe care îl puteți utiliza pentru a vă compila clasa B. Rețineți că acest lucru necesită să puteți analiza codul Java (folosind ceva de genul JavaParser) și rezolvați simboluri. Dacă nu aveți idee de unde să începeți, aruncați o privire la java-symbol-solver.

Instrumente: Gradle, Maven, Test Frameworks, CI

Vestea bună este că puteți face ca faptul că folosesc un modul scris în limba dvs. să fie total transparent pentru utilizator prin dezvoltarea unui plugin pentru gradle sau maven. Puteți instrui sistemul de compilare să compileze fișiere în limbajul dvs. de programare. Utilizatorul va continua să ruleze mvn compile sau gradle assemble și nu va observa nicio diferență.

Vestea proastă este că scrierea plugin-urilor Maven nu este ușoară: documentația este foarte slabă, nu este inteligibilă și în mare parte depășită sau pur și simplu greșită . Da, nu sună reconfortant. Nu am scris încă pluginuri gradle, dar pare mult mai ușor.

Rețineți că ar trebui să luați în considerare și modul în care testele pot fi executate folosind sistemul de compilare. Pentru a susține teste, ar trebui să vă gândiți la un cadru de bază pentru testarea unitară și ar trebui să îl integrați cu sistemul de construcție, astfel încât rularea testului Maven să caute teste în limba dvs., să le compilați și să le rulați raportând rezultatul către utilizator.

Sfatul meu este să te uiți la exemplele disponibile: unul dintre ele este pluginul Maven pentru limbajul de programare Torino.

Odată ce l-ați implementat, toată lumea ar trebui să poată compila cu ușurință fișiere sursă scrise în limba dvs. și să le folosească în servicii de integrare continuă precum Travis.

Plugin IDE

Un plugin pentru un IDE va ​​fi instrumentul cel mai vizibil pentru utilizatorii dvs. și ceva care va afecta foarte mult percepția limbajului dvs. Un plugin bun poate ajuta utilizatorul să învețe limba, oferind completare automată inteligentă, erori contextuale și refactorizări sugerate.

Acum, cea mai comună strategie este să alegeți un IDE (de obicei, Eclipse sau IntelliJ IDEA) și să dezvoltați un plugin specific pentru acesta. Aceasta este probabil cea mai complexă piesă din lanțul dvs. de instrumente. Acesta este cazul din mai multe motive: în primul rând nu puteți reutiliza în mod rezonabil munca pe care o veți cheltui dezvoltând pluginul pentru un IDE pentru celelalte. Eclipse și pluginul dvs. IntelliJ vor fi complet separate. Al doilea punct este că dezvoltarea pluginului IDE este ceva nu foarte comun, așa că nu există prea multă documentație și comunitatea este mică. Înseamnă că va trebui să petreci mult timp descoperind lucruri pentru tine. Am dezvoltat personal plugin-uri pentru Eclipse și pentru IntelliJ IDEA. Întrebările mele de pe forumurile Eclipse au rămas fără răspuns luni sau ani. Pe forumurile IntelliJ am avut mai mult noroc și uneori am primit un răspuns de la dezvoltatori. Cu toate acestea, baza de utilizatori a dezvoltatorilor de pluginuri este mai mică, iar API-urile sunt foarte bizantine. Pregătește-te să suferi.

Există o alternativă la toate acestea și este să folosiți Xtext. Xtext este un cadru pentru dezvoltarea de pluginuri pentru Eclipse, IntelliJ IDEA și web. S-a născut pe Eclipse și a fost recent extins pentru a susține celelalte platforme, așa că nu există atât de multă experiență în acest sens, dar ar putea fi o alternativă demnă de luat în considerare. Permiteți-mi să înțeleg asta: singura modalitate de a dezvolta un plugin foarte bun este să-l dezvoltați folosind API-ul nativ al fiecărui IDE. Cu toate acestea, cu Xtext puteți avea ceva rezonabil decent cu o fracțiune din efort - doar îl dați la sintaxa limbii dvs. și obțineți erori de sintaxă/completare gratuit. Totuși, trebuie să implementați rezoluția simbolului și părțile dure, dar acesta este un punct de plecare foarte interesant; cu toate acestea, biții greu sunt integrarea cu bibliotecile specifice platformei pentru a rezolva simbolurile Java, așa că acest lucru nu va rezolva cu adevărat toate problemele dumneavoastră.

Concluzii

Există multe moduri în care ai putea pierde potențiali utilizatori care și-au arătat interesul pentru limba ta. Adoptarea unei noi limbi este o provocare pentru că necesită învățarea ei și adaptarea obiceiurilor noastre de dezvoltare. Reducând cât mai mult posibil uzura și valorificând ecosistemul deja cunoscut utilizatorilor dvs., îi puteți împiedica pe utilizatori să renunțe înainte să învețe și să se îndrăgostească de limba dvs.

În scenariul ideal, utilizatorul dvs. ar putea clona un proiect simplu scris în limba dvs. și să-l construiască folosind instrumentele standard (Maven sau Gradle) fără a observa nicio diferență. Dacă dorește să editeze proiectul, l-ar putea deschide în editorul său favorit, iar pluginul îl va ajuta să-i sublinieze erorile și să ofere completări inteligente. Acesta este un scenariu mult diferit de a trebui să vă dați seama cum să vă invocați compilatorul și să editați fișierele folosind notepad. Ecosistemul din jurul limbii tale poate face cu adevărat diferența, iar în prezent poate fi construit cu un efort rezonabil.

Sfatul meu este să fii creativ în limba ta, dar nu în instrumentele tale. Reduceți dificultățile inițiale pe care le întâmpină oamenii pentru a vă adopta limba folosind standarde familiare.

Design fericit de limbaj!


Citiți suplimentare pe blogul Toptal Engineering:

  • Cum să abordați scrierea unui interpret de la zero