Për disa vjet kam provuar të zhvilloj gjuhën time të programimit. Kisha dëshirë të krijoja një gjuhë që do të ishte sa më e thjeshtë, funksionale dhe e përshtatshme.
Në këtë artikull dëshiroj të përshkruaj hapat kryesorë të punës sime dhe për t'u nisur, të përshkruaj konceptin e krijuar të gjuhës dhe implementimin e saj të parë mbi të cilin aktualisht po punoj.
Më lejoni të them paraprakisht se kam shkruar të gjithë projektin në Free Pascal, sepse programet në të mund të përdoren në shumë platforma, dhe vetë kompajleri jep binarë mjaft të optimizuar (krijoj të gjitha përbërësit e projektit me flamurin O2).
Mediat e ekzekutimit të gjuhës
E para që duhet të flas është për makinë virtuale, që më është dashur ta shkruaj për të ekzekutuar aplikacionet e ardhshme në gjuhën time. Kam vendosur të realizoj një arkitekturë me stekë, ndoshta sepse ishte më e lehtë. Nuk kam gjetur asnjë artikull normal se si ta bëj këtë në rusisht, e prandaj, pas një njohjeje me materialin anglisht, fillova të dizajnoja dhe shkruaja biçikletën time. Më poshtë do të paraqes idetë dhe zhvillimet e mia "të avancuara" në këtë fushë.
Implementimi i stekës
Mund të sigurohet që adoption e VM ka një stek. Në implementimin tim, ai funksionon me blloqe. Në thelb, ky është një array i thjeshtë i treguesve dhe një variabël për ruajtjen e indeksit të majës së stekut.
Në inicializimin e tij, krijohet një array me 256 elemente. Nëse në stek hidhen më shumë tregues, madhësia e tij rritet me 256 elemente të tjera. Po ashtu, kur elementet hiqen nga steku, madhësia e tij rregullohet.
Në VM përdoren disa steke:
- Steku kryesor.
- Steku për ruajtjen e pikave të kthimit.
- Steku i mbledhësit të plehrave.
- Steku i trajtuesit të bllokimeve try/catch/finally.
Konsistat dhe variablat
Me këtë gjithçka është e thjeshtë. Konstantët përpunohen me një copë të vogël kodi dhe janë të aksesueshme në aplikacione në të ardhmen me adresa statike. Variablat janë një array treguesish me një madhësi të caktuar, ku qasje në selitë e tij bëhet përmes indeksit — dmth. adresës statike. Variablat mund të vendosen në majën e stekut ose të lexohen prej tij. Në fakt, pasi variablat tona thelbësisht mbajnë tregues në vlera në memorien e VM, gjuha dominon në punën me tregues implicit.
Grumbulluesi i mbeturinave
Në VM-në time është gjysmëautomatike. Domethënë, zhvilluesi vetë vendos kur është e nevojshme të thërrasë mbledhësin e mbetjeve. Ai nuk funksionon sipas treguesve të zakonshëm të treguesve, siç është në Python, Perl, Ruby, Lua dhe të tjerë. Ai realizohet përmes një sistemi treguesish. Domethënë, kur supozohet se një variabël atribuojnë një vlerë përkohshme — treguesi për këtë vlerë shtohet në grumbullin e mbledhësit të mbetjeve. Më pas, mbledhësi kalon shpejt përmes listës së përgatitur të treguesve.
Trajtimi i blloqeve try/catch/finally
Siç ndodh në çdo gjuhë moderne, trajtimi i përjashtimeve është një përbërës i rëndësishëm. Në thelbin e VM-së, është i mbështjellë në një bllok try..catch, i cili mund të kthehet në ekzekutimin e kodit, pas kapjes së një përjashtimi, duke vendosur në grumbull pak informacion në lidhje me të. Në kodin e aplikacioneve, mund të vendosen blloqet try/catch/finally të kodit, duke treguar pikët e hyrjes në catch (trajtohet përjashtimin) dhe në finally/end (fundin e bllokut).
Multithreading
Ajo mbështetet në nivelin e VM-së. Është e thjeshtë dhe e lehtë për t’u përdorur. Funksionon pa sistemin e ndërprerjeve, kështu që kodi duhet të ekzekutohet në disa thjesht dhe disa herë më shpejt përkatësisht.
Biblioteka të jashtme për VM-në
Ky është një element i nevojshëm. VM mbështet importet, ashtu siç është realizuar në gjuhë të tjera. Mund të shkruani pjesën e kodit në Mash dhe pjesën tjetër në gjuhë natyrore, duke i lidhur ato në një tërësi.
Përkthyesi nga një gjuhë të nivelit të lartë Mash në bytecode për VM
Gjuha ndërmjetëse
Për të shkruar shpejt një përkthyes nga një gjuhë të ndërlikuar në kod për VM, fillimisht kam zhvilluar një gjuhë ndërmjetëse. Kjo doli të ishte një pamje e tmerrshme, si asamble. Nuk ka kuptim ta shqyrtojmë këtu. Thjesht do të them se në këtë nivel, përkthyesi përpunon shumicën e konstanteve, variablave, llogarit adresat e tyre statike dhe adresat e pikave të hyrjes.
Arkitektura e përkthyesit
Zgjodha një arkitekturë të mos shumë të mirë për realizimin. Përkthyesi nuk ndalon ndërtimin e një peme kodi, siç i takon përkthyesve të tjerë. Ai shikon fillimin e konstrukcionit. Domethënë, nëse pjesa e kodit e shqyrtuar ka formën "while ", është evidente se kjo është një konstrukcion while dhe duhet ta përpunojë si një konstrukcion while. Diku si një switch-case të ndërlikuar.
Falë këtyre zgjidhjeve arkitektonike, përkthyesi rezultoi të jetë i shpejtë, megjithatë, thjeshtësia e modifikimeve të tij u rrit shumë. Konstrukcionet e nevojshme i shtoja më shpejt se sa mund të ftohej kafja ime. Mbështetje e plotë për OOP u realizua për më pak se një javë.
Optimizimi i kodit
Sigurisht, do të kishte mundur të realizohej dhe më mirë (dhe do të realizohet, por më vonë, kur të kem mundësi). Aktualisht, optimizuesi di vetëm të shqiptojë kodin e papërdorur, konstantet dhe importet nga ndërtimi. Gjithashtu, disa konstante me vlera të njëjta zëvendësohen me një të vetme. Kështu që, kjo është ajo.
Gjuha Mash
Koncepsioni kryesor i gjuhës
Idetë kryesore ishin të zhvillohej një gjuhë sa më funksionale dhe e thjeshtë. Besoj se zhvillimi po e kryen detyrën e tij me ndihmën e përkryer.
Blloqet e kodit, procedurat dhe funksionet
Të gjitha konstrukucionet në gjuhë fillojnë me dy pika : dhe përfundojnë me operatorin end.
Procedurat dhe funksionet shpallën si proc dhe func përkatësisht. Brenda prapakohësit listohen argumentet. Të gjitha si në shumicën e gjuhëve të tjera.
Me operatorin kthehu mund të kthehet një vlerë nga funksioni, operatori break lejon daljen nga procedura/funksioni (nëse ndodhet jashtë cikleve).
Shembulli i kodit:
...
func summ(a, b):
return a + b
end
proc main():
println(summ(inputln(), inputln()))
end
Strukturat e mbështetura
- Ciklet: for..end, while..end, until..end
- Kushtet: if..[else..]end, switch..[case..end..][else..]end
- Metodat: proc <emri>():… end, func <emri>():… end
- Label & goto: <emri>:, jump <emri>
- Enum për enumerimet dhe tabulat e konstantave.
Variablat
Translatori mund t'i përcaktojë automatikisht, ose nëse zhvilluesi shkruan var para përcaktimit të tyre.
Shembujt e kodit:
a ?= 10
b ?= a + 20
var a = 10, b = a + 20
Përkrahën variablat globale dhe lokale.
OOP
Ja kemi arritur te tema më e mirë. Në gjuhën Mash përkrahen të gjitha paradigmave të programimit objekt-orientuar. Kështu, klasat, trashëgimet, polimorfizmi (përfshirë dinamik), refleksioni dinamik automatik dhe introspeksioni (i plotë).
Pa fjalë të tepërta, është më mirë të jap shembujt e kodit.
Një klasë e thjeshtë dhe puna me të:
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
Do të nxjerrë: 30.
Trashëgimia dhe polimorfizmi:
përdor <bf>
përdor <crt>
klasa 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
klasa 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
Do të printojë: 60.
Çfarë ka të bëjë me polimorfizmin e dinamik? Po kjo është refleksion!:
përdor <bf>
përdor <crt>
klasa 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
klasa 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
Do të printojë: 60.
Tani, le të kushtojmë një minutë introspeksionit për vlerat e thjeshta dhe klasat:
përdor <bf>
përdor <crt>
klasa MyClass:
var a, b
end
proc main():
x ?= new MyClass
println(BoolToStr(x->type == MyClass))
x->rem()
println(BoolToStr(typeof(3.14) == typeReal))
end
Do të printojë: true, true.
Për operatorët e caktimit dhe treguesit e qartë
Operatori ?= shërben për t'i dhënë një variabli një tregues për një vlerë në memorje.
Operatori = ndryshon vlerën në memorje përmes treguesit nga variabli.
Dhe tani pak për treguesit e qartë. I kam shtuar në gjuhë që ata të ishin.
@<variabli> — merrni një tregues të qartë për variablën.
?<переменная> — получить переменную по указателю.
@= — caktë vlerën e variablit përmes një treguesi të qartë ndaj saj.
Shembulli i kodit:
përdor <bf>
përdor <crt>
proc main():
var a = 10, b
b ?= @a
PrintLn(b)
b ?= ?b
PrintLn(b)
b++
PrintLn(a)
InputLn()
end
Do të shfaqë: një numër, 10, 11.
Provo..[kap..][më në fund..]end
Shembulli i kodit:
përdor <bf>
përdor <crt>
proc main():
println("Fillimi")
provo:
println("Po përpiqem të bëj diçka...")
a ?= 10 / 0
kap:
println(getError())
më në fund:
println("Më në fund")
end
println("Fund")
inputln()
end
Planet për të ardhmen
Po shikoj me vëmend GraalVM & Truffle. Mjedisi im i ekzekutimit nuk ka një përkthyes JIT, kështu që në aspektin e performancës, deri tani mund të konkurojë vetëm me Pythonin. Shpresoj se do të arrij ta realizoj përkthimin JIT mbi bazën e GraalVM ose LLVM.
Repozitori
Mund të eksperimentoni me zhvillimet dhe të ndiqni projektin vetë.
Faleminderit që e lexuat deri në fund, nëse e bëtë këtë.
Burimi: habr.com
