Nouveau langage de programmation Mash

Pendant plusieurs années, j'ai essayé de développer mon propre langage de programmation. Je voulais créer, à mon avis, le langage le plus simple, le plus fonctionnel et le plus pratique possible.

Dans cet article, je souhaite souligner les principales étapes de mon travail et, pour commencer, décrire le concept créé du langage et sa première implémentation, sur laquelle je travaille actuellement.

Laissez-moi vous dire d'avance que j'ai écrit l'intégralité du projet en Free Pascal, parce que... les programmes qui y sont peuvent être assemblés pour un grand nombre de plates-formes, et le compilateur lui-même produit des binaires très optimisés (je collecte tous les composants du projet avec le drapeau O2).

Exécution du langage

Tout d’abord, cela vaut la peine de parler de la machine virtuelle que j’ai dû écrire pour exécuter de futures applications dans mon langage. J'ai peut-être décidé d'implémenter une architecture de pile parce que c'était le moyen le plus simple. Je n'ai trouvé aucun article normal sur la façon de procéder en russe, alors après m'être familiarisé avec le matériel en anglais, je me suis mis à concevoir et à écrire mon propre vélo. Je présenterai ensuite mes idées et développements « avancés » en la matière.

Implémentation de la pile

Évidemment, au sommet de la VM se trouve la pile. Dans mon implémentation, cela fonctionne par blocs. Il s’agit essentiellement d’un simple tableau de pointeurs et d’une variable pour stocker l’index du haut de la pile.
Lors de son initialisation, un tableau de 256 éléments est créé. Si davantage de pointeurs sont placés sur la pile, sa taille augmente des 256 éléments suivants. En conséquence, lors de la suppression d'éléments de la pile, sa taille est ajustée.

La VM utilise plusieurs piles :

  1. Pile principale.
  2. Une pile pour stocker les points de retour.
  3. Pile de collecteur de déchets.
  4. Essayez/catch/finally bloquez la pile du gestionnaire.

Constantes et variables

Celui-ci est simple. Les constantes sont gérées dans un petit morceau de code séparé et sont disponibles dans les futures applications via des adresses statiques. Les variables sont un tableau de pointeurs d'une certaine taille, l'accès à ses cellules s'effectue par index - c'est-à-dire adresse statique. Les variables peuvent être poussées vers le haut de la pile ou lues à partir de là. En fait, parce que Alors que nos variables stockent essentiellement des pointeurs vers des valeurs dans la mémoire de la VM, le langage est dominé par le travail avec des pointeurs implicites.

Éboueur

Dans ma VM, c'est semi-automatique. Ceux. le développeur décide lui-même quand appeler le garbage collector. Cela ne fonctionne pas avec un compteur de pointeurs classique, comme en Python, Perl, Ruby, Lua, etc. Il est mis en œuvre grâce à un système de marqueurs. Ceux. lorsqu'une variable est destinée à se voir attribuer une valeur temporaire, un pointeur vers cette valeur est ajouté à la pile du garbage collector. À l'avenir, le collectionneur parcourt rapidement la liste de pointeurs déjà préparée.

Gestion des blocs try/catch/finally

Comme dans tout langage moderne, la gestion des exceptions est un élément important. Le cœur de la VM est enveloppé dans un bloc try..catch, qui peut revenir à l'exécution du code après avoir intercepté une exception en poussant certaines informations à ce sujet sur la pile. Dans le code d'application, vous pouvez définir des blocs de code try/catch/finally, en spécifiant les points d'entrée à catch (gestionnaire d'exceptions) et enfin/end (fin du bloc).

Multithreading

Il est pris en charge au niveau de la VM. C'est simple et pratique à utiliser. Il fonctionne sans système d'interruption, le code doit donc être exécuté dans plusieurs threads plusieurs fois plus rapidement, respectivement.

Bibliothèques externes pour les machines virtuelles

Il n'y a aucun moyen de s'en passer. La VM prend en charge les importations, de la même manière qu'elle est implémentée dans d'autres langages. Vous pouvez écrire une partie du code dans Mash et une partie du code dans des langues natives, puis les lier en une seule.

Traducteur du langage Mash de haut niveau vers le bytecode pour les VM

Langue intermédiaire

Pour écrire rapidement un traducteur d'un langage complexe vers du code VM, j'ai d'abord développé un langage intermédiaire. Le résultat fut un terrible spectacle de type assembleur qu'il n'y a pas de raison particulière d'envisager ici. Je dirai seulement qu'à ce niveau le traducteur traite la plupart des constantes et des variables, calcule leurs adresses statiques et les adresses des points d'entrée.

Architecture du traducteur

Je n'ai pas choisi la meilleure architecture pour la mise en œuvre. Le traducteur ne construit pas d'arborescence de codes, comme le font les autres traducteurs. Il regarde le début de la structure. Ceux. si le morceau de code analysé ressemble à « while <condition> : », alors il est évident qu'il s'agit d'une construction de boucle while et doit être traitée comme une construction de boucle while. Quelque chose comme un cas de commutation complexe.

Grâce à cette solution architecturale, le traducteur s'est avéré peu rapide. Cependant, la facilité de sa modification a considérablement augmenté. J'ai ajouté les structures nécessaires plus rapidement que mon café ne pouvait refroidir. La prise en charge complète de la POO a été mise en œuvre en moins d'une semaine.

Optimisation du code

Ici, bien sûr, cela aurait pu être mieux mis en œuvre (et le sera, mais plus tard, dès que l’on y parviendra). Jusqu'à présent, l'optimiseur sait uniquement comment supprimer le code inutilisé, les constantes et les importations de l'assembly. De plus, plusieurs constantes ayant la même valeur sont remplacées par une seule. C'est tout.

Langue de purée

Concept de base du langage

L’idée principale était de développer un langage le plus fonctionnel et le plus simple possible. Je pense que le développement s'acquitte de sa tâche avec brio.

Blocs de code, procédures et fonctions

Toutes les constructions de la langue s'ouvrent par deux points. : et sont fermés par l'opérateur fin.

Les procédures et les fonctions sont déclarées respectivement comme proc et func. Les arguments sont listés entre parenthèses. Tout est comme la plupart des autres langues.

Opérateur retourner vous pouvez renvoyer une valeur à partir d'une fonction, d'un opérateur pause permet de sortir de la procédure/fonction (si elle est en dehors des boucles).

Exemple de code:

...

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

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

Conceptions prises en charge

  • Boucles : for..end, while..end, jusqu'à..end
  • Conditions : si..[else..]fin, switch..[case..end..][else..]fin
  • Méthodes : proc <name>():... end, func <name>():... end
  • Étiquette & goto : <nom> :, saute <nom>
  • Énumérations Enum et tableaux constants.

Variables

Le traducteur peut les déterminer automatiquement, ou si le développeur écrit var avant de les définir.

Exemples de codes :

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Les variables globales et locales sont prises en charge.

POO

Eh bien, nous sommes arrivés au sujet le plus délicieux. Mash prend en charge tous les paradigmes de programmation orientée objet. Ceux. classes, héritage, polymorphisme (y compris dynamique), réflexion automatique dynamique et introspection (complète).

Sans plus tarder, il est préférable de simplement donner des exemples de code.

Une classe simple et travailler avec:

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

Sortie : 30.

Héritage et polymorphisme :

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

Sortie : 60.

Qu'en est-il du polymorphisme dynamique ? Oui, c'est une réflexion ! :

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

Sortie : 60.

Prenons maintenant un moment pour introspecter des valeurs et des classes simples :

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

Affichera : vrai, vrai.

À propos des opérateurs d'affectation et des pointeurs explicites

L'opérateur ?= est utilisé pour attribuer à une variable un pointeur vers une valeur en mémoire.
L'opérateur = modifie une valeur en mémoire à l'aide d'un pointeur provenant d'une variable.
Et maintenant un peu sur les pointeurs explicites. Je les ai ajoutés à la langue pour qu'ils existent.
@<variable> — prend un pointeur explicite vers une variable.
?<variable> — récupère une variable par pointeur.
@= — attribue une valeur à une variable par un pointeur explicite vers celle-ci.

Exemple de code:

uses <bf>
uses <crt>

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

Affichera : un nombre, 10, 11.

Essayez..[catch..][enfin..]fin

Exemple de code:

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

Plans pour l'avenir

Je continue de regarder et de regarder GraalVM & Truffle. Mon environnement d'exécution ne dispose pas de compilateur JIT, donc en termes de performances, il n'est actuellement compétitif qu'avec Python. J'espère pouvoir implémenter une compilation JIT basée sur GraalVM ou LLVM.

dépôt

Vous pouvez jouer avec les évolutions et suivre vous-même le projet.

site Web
Dépôt sur GitHub

Merci d'avoir lu jusqu'au bout si vous l'avez fait.

Source: habr.com

Ajouter un commentaire