Negli ultimi anni ho messo alla prova le mie capacità nello sviluppo del mio linguaggio di programmazione. Volevo creare un linguaggio che fosse, a mio avviso, il più semplice, funzionale e comodo possibile.
In questo articolo voglio illustrare le fasi principali del mio lavoro e iniziare descrivendo il concetto di linguaggio creato e la sua prima implementazione su cui sto attualmente lavorando.
Voglio anticipare che ho scritto l'intero progetto in Free Pascal, perché i programmi in esso possono essere compilati per un gran numero di piattaforme, e il compilatore stesso produce anche binary molto ottimizzati (compilo tutte le componenti del progetto con l'opzione O2).
Ambiente di esecuzione del linguaggio
Per prima cosa devo parlare della macchina virtuale che ho dovuto scrivere per eseguire le future applicazioni nel mio linguaggio. Ho deciso di implementare un'architettura basata su stack, probabilmente perché è stata la soluzione più semplice. Non ho trovato alcun articolo utile su come farlo in russo, quindi dopo aver studiato materiale in inglese, mi sono dedicato alla progettazione e scrittura della mia soluzione. Di seguito presenterò le mie idee e sviluppi 'all'avanguardia' in questo campo.
Implementazione dello stack
È ovvio che alla base della VM ci sia uno stack. Nella mia implementazione funziona a blocchi. In sostanza, si tratta di un semplice array di puntatori e di una variabile per memorizzare l'indice della cima dello stack.
Durante l'inizializzazione, viene creato un array di 256 elementi. Se nello stack vengono inseriti più puntatori, la sua dimensione aumenta di ulteriori 256 elementi. Pertanto, quando si rimuovono elementi dallo stack, la sua dimensione viene regolata.
Nella VM sono utilizzati diversi stack:
- Stack principale.
- Stack per la memorizzazione dei punti di ritorno.
- Stack del garbage collector.
- Stack del gestore dei blocchi try/catch/finally.
Costanti e variabili
Con questo è tutto semplice. Le costanti vengono elaborate da un piccolo frammento di codice e sono disponibili nelle applicazioni in futuro a indirizzi statici. Le variabili rappresentano un array di puntatori di dimensione specifica, l'accesso alle sue celle avviene tramite indice, cioè un indirizzo statico. Le variabili possono essere posizionate in cima allo stack o lette da lì. In effetti, poiché le variabili memorizzano essenzialmente puntatori ai valori in memoria della VM, nel linguaggio è predominante l'uso di puntatori impliciti.
Raccoglitore di spazzatura
Nella mia VM è semi-automatica. Cioè, è lo sviluppatore a decidere quando è necessario invocare il garbage collector. Non funziona con il tradizionale contatore di riferimenti, come in linguaggi come Python, Perl, Ruby, Lua, ecc. È realizzato tramite un sistema di marker. Quando si prevede che a una variabile venga assegnato un valore temporaneo, il puntatore a quel valore viene aggiunto allo stack del garbage collector. Successivamente, il garbage collector esamina rapidamente l'elenco già pronto dei puntatori.
Gestione dei blocchi try/catch/finally
Come in qualsiasi linguaggio moderno, la gestione delle eccezioni è una componente importante. Il nucleo della VM è racchiuso in un blocco try..catch, che può riprendere l'esecuzione del codice dopo aver catturato un'eccezione, posizionando nello stack alcune informazioni al riguardo. Nel codice delle applicazioni, è possibile definire blocchi di codice try/catch/finally, specificando i punti di ingresso per catch (gestore delle eccezioni) e per finally/end (fine del blocco).
Multithreading
È supportato a livello di VM. È semplice e comodo da usare. Funziona senza sistemi di interruzione, quindi il codice può essere eseguito in più thread a una velocità significativamente superiore.
Librerie esterne per la VM
È impossibile farne a meno. La VM supporta gli import, proprio come avviene in altri linguaggi. Puoi scrivere parte del codice in Mash e parte in linguaggi nativi, per poi unirli in un tutto unico.
Traduttore da linguaggio di alto livello Mash a bytecode per la VM
Linguaggio intermedio
Per scrivere rapidamente un traduttore da un linguaggio complesso al codice per la VM, ho prima sviluppato un linguaggio intermedio. È venuto fuori un mostro simile a un assemblatore, che non ha molto senso considerare qui. Posso solo dire che a questo livello il traduttore gestisce la maggior parte delle costanti, delle variabili, calcolando i loro indirizzi statici e gli indirizzi dei punti di ingresso.
Architettura del traduttore
Ho scelto un'architettura non proprio eccellente per l'implementazione. Il traduttore non costruisce un albero del codice, come dovrebbero fare gli altri traduttori. Guarda all'inizio della costruzione. Cioè, se il pezzo di codice analizzato è del tipo «while :», è ovvio che si tratta di una costruzione di ciclo while e deve essere elaborata come tale. È qualcosa di simile a uno switch-case complesso.
Grazie a questa soluzione architettonica, il traduttore non è risultato molto veloce. Tuttavia, la facilità di modifica è aumentata notevolmente. Ho aggiunto le strutture necessarie più velocemente di quanto il mio caffè potesse raffreddarsi. Il supporto completo per la programmazione orientata agli oggetti è stato realizzato in meno di una settimana.
Ottimizzazione del codice
Qui si sarebbe potuto fare di meglio (e si farà in futuro, quando ne avrò il tempo). Per ora, l'ottimizzatore è in grado di rimuovere solo il codice non utilizzato, le costanti e le importazioni dalla build. Inoltre, diverse costanti con lo stesso valore vengono sostituite da una sola. Questo è tutto.
Lingua Mash
Concetto principale della lingua
L'idea principale era sviluppare un linguaggio il più funzionale e semplice possibile. Credo che gli sviluppatori abbiano affrontato il compito con grande successo.
Blocchi di codice, procedure e funzioni
Tutte le strutture nel linguaggio si aprono con due punti : e si chiudono con l'operatore end.
Le procedure e le funzioni vengono dichiarate come proc e func, rispettivamente. Gli argomenti vengono elencati tra parentesi, come nella maggior parte degli altri linguaggi.
Con l'operatore return è possibile restituire un valore dalla funzione, l'operatore break consente di uscire dalla procedura/funzione (se si trova al di fuori dei cicli).
Esempio di codice:
...
func summ(a, b):
return a + b
end
proc main():
println(summ(inputln(), inputln()))
end
Strutture supportate
- Cicli: for..end, while..end, until..end
- Condizioni: if..[else..]end, switch..[case..end..][else..]end
- Metodi: proc ():… end, func ():… end
- Label & goto: :, jump
- Enum di enumerazioni e array costanti.
Variabili
Il traduttore può identificarli automaticamente, oppure se lo sviluppatore scrive var prima della loro definizione.
Esempi di codice:
a ?= 10
b ?= a + 20
var a = 10, b = a + 20
Supporto per variabili globali e locali.
OOP
Adesso arriviamo all'argomento più interessante. Nel linguaggio Mash sono supportate tutte le paradigmi della programmazione orientata agli oggetti, ovvero classi, ereditarietà, polimorfismo (incluso quello dinamico), automazione dinamica della riflessione e introspezione (completa).
Senza ulteriori indugi, è meglio fornire semplicemente esempi di codice.
Una classe semplice e il suo utilizzo:
uses
uses
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
Restituisce: 30.
Ereditarietà e polimorfismo:
usa <bf>
usa <crt>
classe MyClass:
var a, b
proc Create, Free
func Summ
fine
proc MyClass::Create(a, b):
$a = new(a)
$b = new(b)
fine
proc MyClass::Free():
Free($a, $b)
$rem()
fine
func MyClass::Summ():
ritorna $a + $b
fine
classe MyNewClass(MyClass):
func Summ
fine
func MyNewClass::Summ():
ritorna ($a + $b) * 2
fine
proc main():
x ?= new MyNewClass(10, 20)
println(x->Summ())
x->Free()
fine
Stampa: 60.
E riguardo al polimorfismo dinamico? Si tratta di riflessione!:
usa <bf>
usa <crt>
classe MyClass:
var a, b
proc Create, Free
func Summ
fine
proc MyClass::Create(a, b):
$a = new(a)
$b = new(b)
fine
proc MyClass::Free():
Free($a, $b)
$rem()
fine
func MyClass::Summ():
ritorna $a + $b
fine
classe MyNewClass(MyClass):
func Summ
fine
func MyNewClass::Summ():
ritorna ($a + $b) * 2
fine
proc main():
x ?= new MyClass(10, 20)
x->Summ ?= MyNewClass::Summ
println(x->Summ())
x->Free()
fine
Stampa: 60.
Ora prendiamoci un minuto di introspezione per i valori semplici e le classi:
usa <bf>
usa <crt>
classe MyClass:
var a, b
fine
proc main():
x ?= new MyClass
println(BoolToStr(x->type == MyClass))
x->rem()
println(BoolToStr(typeof(3.14) == typeReal))
fine
Stampa: true, true.
Sugli operatori di assegnazione e puntatori espliciti
L'operatore ?= serve per assegnare a una variabile un puntatore a un valore in memoria.
L'operatore = modifica il valore in memoria tramite il puntatore della variabile.
E ora un po' sui puntatori espliciti. Li ho aggiunti al linguaggio affinché ci siano.
@<variabile> — prendi un puntatore esplicito alla variabile.
?<переменная> — получить переменную по указателю.
@= — assegnare un valore a una variabile tramite un puntatore esplicito.
Esempio di codice:
usa
usa
proc main():
var a = 10, b
b ?= @a
PrintLn(b)
b ?= ?b
PrintLn(b)
b++
PrintLn(a)
InputLn()
end
Stamperà: un numero qualsiasi, 10, 11.
Try..[catch..][finally..]end
Esempio di codice:
usa
usa
proc main():
println("Inizio")
try:
println("Provo a fare qualcosa...")
a ?= 10 / 0
catch:
println(getError())
finally:
println("Finalmente")
end
println("Fine")
inputln()
end
Piani per il futuro
Sto continuando a esplorare GraalVM & Truffle. Il mio ambiente di esecuzione non ha un compilatore JIT, quindi in termini di prestazioni può competere solo con Python. Spero di riuscire a implementare la compilazione JIT basata su GraalVM o LLVM.
Repository
Puoi sperimentare con i lavori in corso e seguire il progetto da solo.
Grazie per aver letto fino alla fine, se lo hai fatto.
Fonte: habr.com
