Cum aș putea crea un limbaj de programare?

Titlul acestui articol reflectă o întrebare pe care o aud mereu pe forumuri sau în e-mailurile pe care le primesc.

Cred că toți dezvoltatorii curioși și-au pus-o cel puțin o dată. Este normal să fii fascinat de modul în care funcționează limbajele de programare. Din păcate, majoritatea răspunsurilor pe care le citim sunt foarte academice sau teoretice. Altele conțin prea multe detalii de implementare. După ce le citim încă ne întrebăm cum funcționează lucrurile în practică.

Așa că vom răspunde. Da, vom vedea care este procesul de creare a propriului limbaj complet, cu un compilator pentru el și ce nu.

Prezentare generală

Majoritatea persoanelor care vor să învețe cum să „creeze un limbaj de programare” caută efectiv informații despre cum să construiască un compilator. Ei vor să înțeleagă mecanismele care permit executarea unui nou limbaj de programare.
Un compilator este o piesă fundamentală a puzzle-ului, dar realizarea unui nou limbaj de programare necesită mai mult decât atât:

1) Un limbaj trebuie să fie proiectat: creatorul limbajului trebuie să ia unele decizii fundamentale cu privire la paradigmele care vor fi folosite și la sintaxa limbajului
2) Trebuie creat un compilator
3) Trebuie implementată o bibliotecă standard
4) Trebuie furnizate instrumente de suport, cum ar fi editori și sisteme de construire

Să vedem mai în detaliu ce presupune fiecare dintre aceste puncte.

Desenarea unui limbaj de programare

Dacă doriți doar să vă scrieți propriul compilator pentru a învăța cum funcționează aceste lucruri, puteți sări peste această fază. Puteți doar să luați un subset al unui limbaj existent sau să veniți cu o variantă simplă a acestuia și să începeți. Cu toate acestea, dacă aveți planuri de a vă crea propriul limbaj de programare, va trebui să vă gândiți puțin la asta.

Cred că proiectarea unui limbaj de programare este împărțită în două faze:

  1. Faza de imagine de ansamblu
  2. Faza de rafinare

În prima fază răspundem la întrebările fundamentale despre limbajul nostru.

  • Ce paradigmă de execuție dorim să folosim? Va fi imperativă sau funcțională? Sau poate bazată pe mașini de stare sau pe reguli de afaceri?
  • Vrem o tipizare statică sau o tipizare dinamică?
  • Ce fel de programe va fi cel mai bine pentru acest limbaj? Va fi folosit pentru scripturi mici sau pentru sisteme mari?
  • Ce contează cel mai mult pentru noi: performanța? Lizibilitate?
  • Vrem ca acesta să fie similar cu un limbaj de programare existent? Va fi destinat dezvoltatorilor C sau va fi ușor de învățat pentru cei care vin din Python?
  • Vrem să funcționeze pe o anumită platformă (JVM, CLR)?
  • Ce fel de capacități de metaprogramare dorim să suporte, dacă există? Macros? Șabloane? Reflecție?

În a doua fază vom continua să dezvoltăm limbajul pe măsură ce îl folosim. Vom da peste probleme, peste lucruri care sunt foarte greu sau imposibil de exprimat în limbajul nostru și vom sfârși prin a-l face să evolueze. A doua fază s-ar putea să nu fie la fel de fermecătoare ca prima, dar este faza în care continuăm să ne ajustăm limbajul pentru a-l face utilizabil în practică, așa că nu ar trebui să o subestimăm.

Construirea unui compilator

Construirea unui compilator este cel mai interesant pas în crearea unui limbaj de programare. Odată ce avem un compilator, putem efectiv să dăm viață limbajului nostru. Un compilator ne permite să începem să ne jucăm cu limbajul, să îl folosim și să identificăm ceea ce ne lipsește în proiectarea inițială. Ne permite să vedem primele rezultate. Este greu de învins bucuria de a executa primul program scris în noul nostru limbaj de programare, oricât de simplu ar fi acel program.

Dar cum construim un compilator?

Ca orice lucru complex, facem acest lucru în pași:

  1. Construim un parser: parserul este partea din compilatorul nostru care ia textul programelor noastre și înțelege ce comenzi exprimă acestea. Recunoaște expresiile, declarațiile, clasele și creează structuri de date interne pentru a le reprezenta. Restul parserului va lucra cu aceste structuri de date, nu cu textul original
  2. (opțional) Traducem arborele de analiză într-un arbore sintactic abstract. În mod obișnuit, structurile de date produse de parser sunt puțin cam de nivel scăzut, deoarece conțin o mulțime de detalii care nu sunt esențiale pentru compilatorul nostru. Din acest motiv, dorim frecvent să rearanjăm structurile de date în ceva puțin mai de nivel superior
  3. Rezolvăm simbolurile. În cod scriem lucruri de genul a + 1. Compilatorul nostru trebuie să își dea seama la ce se referă a. Este un câmp? Este o variabilă? Este un parametru de metodă? Examinăm codul pentru a răspunde la această întrebare
  4. Validăm arborele. Trebuie să verificăm dacă programatorul nu a comis erori. Încearcă el să adune un boolean și un int? Sau accesează un câmp inexistent? Trebuie să producem mesaje de eroare adecvate
  5. Generăm codul mașină. În acest moment traducem codul în ceva pe care mașina îl poate executa. Poate fi un cod mașină propriu-zis sau un bytecode pentru o anumită mașină virtuală
  6. (opțional) Realizăm legătura. În unele cazuri, trebuie să combinăm codul mașină produs pentru programele noastre cu codul bibliotecilor statice pe care dorim să le includem, pentru a genera un singur executabil

Avem întotdeauna nevoie de un compilator? Nu. Îl putem înlocui cu alte mijloace de execuție a codului:

  • Potem scrie un interpretor: un interpretor este, în esență, un program care face pașii 1-4 ai unui compilator și apoi execută direct ceea ce este specificat de Arborele de Sintaxă Abstractă
  • Potem scrie un transpiler: un transpiler va face ceea ce este specificat în etapele 1-4 și apoi va produce un cod într-un limbaj pentru care avem deja un compilator (de exemplu C++ sau Java)

Aceste două alternative sunt perfect valabile și, în mod frecvent, este logic să alegem una dintre ele deoarece efortul necesar este de obicei mai mic.

Am scris un articol în care explicăm cum se scrie un transpiler. Aruncați o privire pe el dacă doriți să vedeți un exemplu practic, cu cod.

În acest articol explicăm mai detaliat diferența dintre un compilator și un interpretor.

O bibliotecă standard pentru limbajul dumneavoastră de programare

Care limbaj de programare trebuie să facă câteva lucruri:

  • Imprimarea pe ecran
  • Accesul la sistemul de fișiere
  • Utilizarea conexiunilor de rețea
  • Crearea de interfețe grafice

Acestea sunt funcționalitățile de bază pentru a interacționa cu restul sistemului. Fără ele, un limbaj este practic inutil. Cum asigurăm aceste funcționalități? Prin crearea unei biblioteci standard. Aceasta va fi un set de funcții sau clase care pot fi apelate în programele scrise în limbajul nostru de programare, dar care vor fi scrise într-un alt limbaj. De exemplu, multe limbaje au biblioteci standard scrise cel puțin parțial în C.

O bibliotecă standard poate conține apoi mult mai mult. De exemplu, clase pentru a reprezenta colecțiile principale, cum ar fi listele și hărțile, sau pentru a procesa formate comune, cum ar fi JSON sau XML. Adesea, va conține funcționalități avansate pentru a procesa șiruri de caractere și expresii regulate.

Cu alte cuvinte, scrierea unei biblioteci standard reprezintă multă muncă. Nu este plină de farmec, nu este la fel de interesantă din punct de vedere conceptual ca scrierea unui compilator, dar este totuși o componentă fundamentală pentru a face un limbaj de programare viabil.

Există modalități de a evita această cerință. Una este de a face limbajul să ruleze pe o anumită platformă și de a face posibilă reutilizarea bibliotecii standard a unui alt limbaj. De exemplu, toate limbajele care rulează pe JVM pot pur și simplu să reutilizeze biblioteca standard Java.

Instrumente de suport pentru un nou limbaj de programare

Pentru a face un limbaj utilizabil în practică, avem frecvent nevoie să scriem câteva instrumente de suport.

Cel mai evident este un editor. Un editor specializat cu evidențiere a sintaxei, verificare a erorilor în linie și autocompletare este în zilele noastre o necesitate pentru a face productiv orice dezvoltator.

Dar astăzi dezvoltatorii sunt răsfățați și se vor aștepta la tot felul de alte instrumente de suport. De exemplu, un depanator ar putea fi foarte util pentru a rezolva un bug neplăcut. Sau un sistem de construire similar cu maven sau gradle ar putea fi ceva ce utilizatorii vor cere mai târziu.

La început, un editor ar putea fi suficient, dar pe măsură ce baza de utilizatori crește, de asemenea, complexitatea proiectelor va crește și vor fi necesare mai multe instrumente de sprijin. Să sperăm că în acel moment va exista o comunitate dispusă să ajute la construirea acestora.

Summary

Crearea unui limbaj de programare este un proces care pare misterios pentru mulți dezvoltatori. În acest articol am încercat să arătăm că este doar un proces. Este fascinant și nu este ușor, dar se poate face.

Puteți dori să construiți un limbaj de programare dintr-o varietate de motive. Un motiv bun este pentru distracție, un altul este pentru a învăța cum funcționează compilatoarele. Limbajul dumneavoastră ar putea sfârși prin a fi foarte util sau nu, în funcție de mulți factori. Cu toate acestea, dacă vă distrați și/sau învățați în timp ce îl construiți, atunci merită să investiți ceva timp în acest sens.

Și, bineînțeles, veți putea să vă lăudați cu colegii dumneavoastră programatori.

Dacă doriți să aflați mai multe despre crearea unui limbaj, aruncați o privire la celelalte resurse pe care le-am creat: Învățați cum să construiți limbaje.

S-ar putea să vă intereseze și unele dintre articolele noastre:

Lasă un răspuns

Adresa ta de email nu va fi publicată.