Noul limbaj de programare Mash

Timp de câțiva ani am încercat să-mi dezvolt propriul limbaj de programare. Am vrut să creez, în opinia mea, cel mai simplu, complet funcțional și convenabil limbaj posibil.

În acest articol vreau să evidențiez principalele etape ale muncii mele și, pentru început, să descriu conceptul creat de limbaj și prima sa implementare, la care lucrez în prezent.

Să spun în avans că am scris întreg proiectul în Free Pascal, pentru că... programele de pe acesta pot fi asamblate pentru un număr mare de platforme, iar compilatorul în sine produce binare foarte optimizate (colectez toate componentele proiectului cu steagul O2).

Timp de rulare a limbii

În primul rând, merită să vorbim despre mașina virtuală pe care a trebuit să o scriu pentru a rula viitoare aplicații în limba mea. Am decis să implementez o arhitectură stivă, probabil, pentru că era cea mai ușoară cale. Nu am găsit un singur articol normal despre cum să fac asta în rusă, așa că, după ce m-am familiarizat cu materialul în limba engleză, m-am așezat să-mi proiectez și să scriu propria bicicletă. În continuare voi prezenta ideile și evoluțiile mele „avansate” în această chestiune.

Implementarea stivei

Evident, în partea de sus a VM-ului se află stiva. În implementarea mea funcționează în blocuri. În esență, aceasta este o matrice simplă de pointeri și o variabilă pentru a stoca indexul din partea de sus a stivei.
Când este inițializat, este creată o matrice de 256 de elemente. Dacă mai multe pointere sunt împinse pe stivă, dimensiunea acestuia crește cu următoarele 256 de elemente. În consecință, la îndepărtarea elementelor din stivă, dimensiunea acestuia este ajustată.

VM utilizează mai multe stive:

  1. Stiva principală.
  2. O stivă pentru stocarea punctelor de retur.
  3. Stiva de colectare a gunoiului.
  4. Încercați/prindeți/în final blocați stiva de handler.

Constante și variabile

Acesta este simplu. Constantele sunt gestionate într-o mică bucată de cod separată și sunt disponibile în aplicațiile viitoare prin adrese statice. Variabilele sunt o serie de pointeri de o anumită dimensiune, accesul la celulele sale se realizează prin index - adică. adresa statica. Variabilele pot fi împinse în partea de sus a stivei sau pot fi citite de acolo. De fapt, pentru că În timp ce variabilele noastre stochează în esență pointeri către valori în memoria VM, limbajul este dominat de lucrul cu pointeri impliciti.

Colector de gunoi

În VM-ul meu este semi-automat. Acestea. dezvoltatorul însuși decide când să cheme gunoiul. Nu funcționează folosind un contor de indicator obișnuit, ca în Python, Perl, Ruby, Lua etc. Este implementat printr-un sistem de marcare. Acestea. când unei variabile se intenționează să i se atribuie o valoare temporară, se adaugă un pointer către această valoare în stiva colectorului de gunoi. În viitor, colecționarul parcurge rapid lista de indicatoare deja pregătită.

Gestionarea blocurilor try/catch/finally

Ca în orice limbă modernă, gestionarea excepțiilor este o componentă importantă. Nucleul VM este împachetat într-un bloc try..catch, care poate reveni la execuția codului după prinderea unei excepții prin împingerea unor informații despre aceasta în stivă. În codul aplicației, puteți defini blocuri de cod try/catch/finally, specificând punctele de intrare la catch (gestionarul de excepții) și finally/end (sfârșitul blocului).

Multithreading

Este suportat la nivel de VM. Este simplu și convenabil de utilizat. Funcționează fără un sistem de întrerupere, deci codul ar trebui să fie executat în mai multe fire, respectiv de câteva ori mai rapid.

Biblioteci externe pentru VM

Nu există nicio modalitate de a face fără asta. VM acceptă importuri, similar modului în care este implementat în alte limbi. Puteți scrie o parte a codului în Mash și o parte a codului în limbile native, apoi le puteți lega într-una singură.

Traducător din limbajul Mash de nivel înalt în bytecode pentru VM

Limbaj intermediar

Pentru a scrie rapid un traducător dintr-un limbaj complex în cod VM, am dezvoltat mai întâi un limbaj intermediar. Rezultatul a fost un spectacol groaznic asemănător unui asamblator, pe care nu are rost să îl luăm în considerare aici. Voi spune doar că la acest nivel traducătorul procesează majoritatea constantelor și variabilelor, calculează adresele lor statice și adresele punctelor de intrare.

Arhitectura traducătorului

Nu am ales cea mai bună arhitectură pentru implementare. Traducătorul nu construiește un arbore de cod, așa cum fac alți traducători. Se uită la începutul structurii. Acestea. dacă fragmentul de cod care este analizat arată ca „while <condition>:”, atunci este evident că acesta este o construcție a buclei while și trebuie procesată ca o construcție a buclei while. Ceva de genul unui caz de comutare complex.

Datorită acestei soluții arhitecturale, traducătorul s-a dovedit a nu fi foarte rapid. Cu toate acestea, ușurința modificării sale a crescut semnificativ. Am adăugat structurile necesare mai repede decât s-ar putea răci cafeaua mea. Suportul OOP complet a fost implementat în mai puțin de o săptămână.

Optimizarea codului

Aici, desigur, ar fi putut fi implementat mai bine (și va fi implementat, dar mai târziu, de îndată ce se ajunge la el). Până acum, optimizatorul știe doar cum să taie codul neutilizat, constantele și importurile din asamblare. De asemenea, mai multe constante cu aceeași valoare sunt înlocuite cu una. Asta e tot.

Limbă Mash

Conceptul de bază al limbajului

Ideea principală a fost de a dezvolta cel mai funcțional și simplu limbaj posibil. Cred că dezvoltarea își face față sarcinii cu un bang.

Blocuri de cod, proceduri și funcții

Toate construcțiile din limbă sunt deschise cu două puncte. : și sunt închise de către operator capăt.

Procedurile și funcțiile sunt declarate ca proc și, respectiv, func. Argumentele sunt enumerate în paranteze. Totul este ca majoritatea celorlalte limbi.

Operator reveni puteți returna o valoare de la o funcție, operator rupe vă permite să părăsiți procedura/funcția (dacă este în afara buclelor).

Cod simplu:

...

func summ(a, b):
  return a + b
end

proc main():
  println(summ(inputln(), inputln()))
end

Modele acceptate

  • Bucle: for..end, while..end, până la..end
  • Condiții: if..[else..]end, switch..[case..end..][else..]end
  • Metode: proc <name>():... end, func <name>():... end
  • Etichetați și mergeți la: <nume>:, săriți <nume>
  • Enumerări enumerate și matrice constante.

variabile

Traducătorul le poate determina automat sau dacă dezvoltatorul scrie var înainte de a le defini.

Exemple de cod:

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Sunt acceptate variabilele globale și locale.

OOP

Ei bine, am ajuns la cel mai delicios subiect. Mash acceptă toate paradigmele de programare orientată pe obiecte. Acestea. clase, moștenire, polimorfism (inclusiv dinamic), reflexie automată dinamică și introspecție (completă).

Fără prea mult timp, este mai bine să dați doar exemple de cod.

O clasă simplă și lucrul cu ea:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

proc main():
  x ?= new MyClass(10, 20)
  println(x->Summ())
  x->Free()
end

Va iesi: 30.

Moștenirea și polimorfismul:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyNewClass(10, 20)
  println(x->Summ())
  x->Free()
end

Va iesi: 60.

Dar polimorfismul dinamic? Da, aceasta este o reflecție!:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyClass(10, 20)
  x->Summ ?= MyNewClass::Summ
  println(x->Summ())
  x->Free()
end

Va iesi: 60.

Acum să luăm un moment pentru a introspecta valorile și clasele simple:

uses <bf>
uses <crt>

class MyClass:
  var a, b
end

proc main():
  x ?= new MyClass
  println(BoolToStr(x->type == MyClass))
  x->rem()
  println(BoolToStr(typeof(3.14) == typeReal))
end

Va ieși: adevărat, adevărat.

Despre operatorii de atribuire și indicatorii expliciți

Operatorul ?= este folosit pentru a atribui unei variabile un pointer unei valori din memorie.
Operatorul = modifică o valoare din memorie folosind un pointer dintr-o variabilă.
Și acum puțin despre indicații explicite. Le-am adăugat la limbă, astfel încât să existe.
@<variabilă> — luați un pointer explicit către o variabilă.
?<variabilă> — obțineți o variabilă prin indicator.
@= — atribuiți o valoare unei variabile printr-un pointer explicit către aceasta.

Cod simplu:

uses <bf>
uses <crt>

proc main():
  var a = 10, b
  b ?= @a
  PrintLn(b)
  b ?= ?b
  PrintLn(b)
  b++
  PrintLn(a)
  InputLn()
end

Va scoate: un număr, 10, 11.

Încearcă..[prinde..][în final..]termină

Cod simplu:

uses <bf>
uses <crt>

proc main():
  println("Start")
  try:
    println("Trying to do something...")
    a ?= 10 / 0
  catch:
    println(getError())
  finally:
    println("Finally")
  end
  println("End")
  inputln()
end

Planurile de viitor

Mă tot uit și mă uit la GraalVM & Truffle. Mediul meu de rulare nu are un compilator JIT, așa că în ceea ce privește performanța este în prezent competitiv doar cu Python. Sper că voi putea implementa compilarea JIT bazată pe GraalVM sau LLVM.

repertoriu

Puteți să vă jucați cu evoluțiile și să urmăriți singur proiectul.

Loc
Depozitul pe GitHub

Vă mulțumesc că ați citit până la sfârșit dacă ați făcut-o.

Sursa: www.habr.com

Adauga un comentariu