MyLang est un langage de programmation éducatif simple inspiré de Python
, JavaScript
et C
, écrit comme un défi personnel, en peu de temps, principalement pour s'amuser à écrire un analyseur de descente récursif et explorer le monde des interprètes. Ne vous attendez pas à un langage de script complet avec des bibliothèques et des frameworks prêts à être utilisés en production. Cependant, MyLang
possède un ensemble minimal de fonctionnalités intégrées et peut également être utilisé à des fins pratiques.
MyLang est écrit en C++17 portable : pour le moment, le projet n'a pas de dépendances autres que la bibliothèque C++ standard. Pour le construire, si GNU make
est installé, exécutez simplement :
$ make -j
Sinon, transmettez simplement tous les fichiers .cpp à votre compilateur et ajoutez le répertoire src/
au chemin de recherche d'inclusion. L'un des aspects les plus agréables du fait de ne pas avoir de dépendances est qu'il n'est pas nécessaire d'avoir un système de build pour les builds uniques.
Passez simplement l'option BUILD_DIR
pour make
:
$ make -j BUILD_DIR=other_build_directory
Si vous souhaitez également exécuter les tests de MyLang, il vous suffit de compiler avec TESTS=1 et de désactiver les optimisations avec OPT=0, pour une meilleure expérience de débogage :
$ make -j TESTS=1 OPT=0
Ensuite, lancez tous les tests avec :
$ ./build/mylang -rt
Il convient de noter que, même si les frameworks de test comme GoogleTest et Boost.Test sont infiniment plus puissants et flexibles que le moteur de test trivial que nous avons dans src/tests.cpp
, ce sont des dépendances externes . Moins il y a de dépendances, mieux c'est, non ? :-)
La façon la plus courte de décrire MyLang
est : un langage python dynamique d'aspect C. Le moyen le plus rapide d'apprendre ce langage est probablement de consulter les scripts dans le répertoire samples/
tout en consultant la courte documentation ci-dessous.
MyLang
est un langage de typage dynamique, comme Python
. Si vous connaissez Python
et que vous souhaitez utiliser des accolades { }
, vous pourrez automatiquement l'utiliser. Aucune surprise. Les chaînes sont immuables comme en Python
, les tableaux peuvent être définis en utilisant [ ]
comme en Python
, et les dictionnaires peuvent également être définis en utilisant { }
. Le langage prend également en charge les tranches de tableau en utilisant la même syntaxe [start:end]
utilisée par Python
.
Cela dit, MyLang
diffère de Python
et des autres langages de script sur plusieurs aspects :
Les constantes de temps d'analyse déclarées à l'aide const
sont prises en charge.
Toutes les variables doivent être déclarées à l'aide var
.
Les variables ont une portée comme en C
. L'observation est prise en charge lorsqu'une variable est explicitement re-déclarée à l'aide var
, dans un bloc imbriqué.
Toutes les instructions d'expression doivent se terminer par ;
comme en C
, C++
et Java
.
Les mots-clés true
et false
existent, mais il n'y a pas de type boolean
. Comme en C, 0
est faux, tout le reste est true
. Cependant, les chaînes, les tableaux et les dictionnaires ont une valeur booléenne, exactement comme en Python
(par exemple, un tableau vide est considéré false
). Le true
intégré n'est qu'un alias pour l'entier 1
.
L'opérateur d'affectation =
peut être utilisé comme en C
, dans des expressions, mais l'opérateur virgule n'existe pas, en raison de la fonctionnalité d'expansion de tableau.
MyLang prend en charge à la fois la boucle for
classique et une boucle foreach
explicite.
MyLang ne prend pas en charge les types personnalisés pour le moment. Cependant, les dictionnaires prennent en charge un sucre syntaxique intéressant : en plus de la syntaxe principale d["key"]
, pour les clés de chaîne, la syntaxe d.key
est également prise en charge.
Les variables sont toujours déclarées avec var
et vivent dans la portée dans laquelle elles ont été déclarées (tout en étant visibles dans les portées imbriquées). Par exemple:
# Variable declared in the global scope
var a = 42 ;
{
var b = 12 ;
# Here we can see both `a` and `b`
print ( a , b ) ;
}
# But here we cannot see `b` .
Il est possible de déclarer plusieurs variables en utilisant la syntaxe familière suivante :
var a , b , c ;
Mais il y a un bémol, probablement la seule fonctionnalité "surprenante" de MyLang
: l'initialisation des variables ne fonctionne pas comme en C. Considérez l'instruction suivante :
var a , b , c = 42 ;
Dans ce cas, au lieu de simplement déclarer a
et b
et d'initialiser c
à 42, nous initialisons les trois variables à la valeur 42. Pour initialiser chaque variable à une valeur différente, utilisez la syntaxe d'expansion de tableau :
var a , b , c = [ 1 , 2 , 3 ] ;
Les constantes sont déclarées de la même manière que les variables , mais elles ne peuvent pas être masquées dans des étendues imbriquées. Par exemple:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
Dans MyLang
les constantes sont évaluées au moment de l'analyse , de la même manière que les déclarations constexpr de C++
(mais nous parlons là de moment de la compilation ). Lors de l'initialisation d'un const
, n'importe quel type de littéral peut être utilisé en plus de l'ensemble des const intégrés. Par exemple:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
Pour comprendre comment exactement une constante a été évaluée, exécutez l'interpréteur avec l'option -s
pour vider l' arborescence de syntaxe abstraite avant d'exécuter le script. Pour l'exemple ci-dessus :
$ cat > t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
$ ./build/mylang t
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
)
--------------------------
Surpris? Eh bien, les constantes autres que les tableaux et les dictionnaires ne sont même pas instanciées en tant que variables. Ils n'existent tout simplement pas au moment de l'exécution . Ajoutons une instruction utilisant x
:
$ cat >> t
print(x);
$ cat t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
print(x);
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
CallExpr(
Id("print")
ExprList(
"hello world a,b,c"
)
)
)
--------------------------
hello world a,b,c
Désormais, tout devrait avoir un sens. Presque la même chose se produit avec les tableaux et les dictionnaires, à l'exception du fait que ces derniers sont également instantanés au moment de l'exécution, afin d'éviter d'avoir des littéraux potentiellement énormes partout. Prenons l'exemple suivant :
$ ./build/mylang -s -e 'const ar=range(4); const s=ar[2:]; print(ar, s, s[0]);'
Syntax tree
--------------------------
Block(
ConstDecl(
Id("ar")
Op '='
LiteralArray(
Int(0)
Int(1)
Int(2)
Int(3)
)
)
ConstDecl(
Id("s")
Op '='
LiteralArray(
Int(2)
Int(3)
)
)
CallExpr(
Id("print")
ExprList(
Id("ar")
Id("s")
Int(2)
)
)
)
--------------------------
[0, 1, 2, 3] [2, 3] 2
Comme vous pouvez le voir, l'opération slice a été évaluée au moment de l'analyse lors de l'initialisation de la constante s
, mais les deux tableaux existent également au moment de l'exécution. Les opérations d'indice sur les expressions const, à la place, sont converties en littéraux. Cela semble être un bon compromis en termes de performances : les petites valeurs telles que les entiers, les flottants et les chaînes sont converties en littéraux lors de l' évaluation const , tandis que les tableaux et les dictionnaires (potentiellement grands) sont laissés sous forme de symboles en lecture seule au moment de l'exécution, tout en permettant certaines opérations sur eux (comme [index]
et len(arr)
) doivent être évaluées de manière constante.
MyLang
prend en charge, pour le moment, uniquement les types (intégrés) suivants :
None Le type de none
, l'équivalent de None
de Python. Les variables qui viennent d'être déclarées sans qu'une valeur leur soit attribuée n'ont none
valeur (par exemple var x;
). La même chose s'applique aux fonctions qui n'ont pas de valeur de retour. En outre, il est utilisé comme valeur spéciale par des éléments intégrés comme find()
, en cas d'échec.
Entier Un entier signé de la taille d'un pointeur (par exemple 3
).
Float Un nombre à virgule flottante (par exemple 1.23
). En interne, c'est un long doublé.
String Une chaîne comme "bonjour". Les chaînes sont immuables et prennent en charge les tranches (par exemple s[3:5]
ou s[3:]
ou s[-2:]
, ayant la même signification qu'en Python
).
Array Un type mutable pour les tableaux et les tuples (par exemple [1,2,3]
). Il peut contenir des éléments de différents types et prend en charge les tranches inscriptibles. Les tranches de tableau se comportent comme des copies tandis que, sous le capot, elles utilisent des techniques de copie sur écriture.
Dictionnaire Les dictionnaires sont des cartes de hachage définies à l'aide de la syntaxe Python
: {"a": 3, "b": 4}
. Les éléments sont accessibles avec la syntaxe familière d["key-string"]
ou d[23]
, peuvent être recherchés avec find()
et supprimés avec erase()
. Pour le moment, seules les chaînes, les entiers et les flottants peuvent être utilisés comme clés d'un dictionnaire. Avantages : les clés de chaîne de type identifiant sont également accessibles avec la syntaxe "membre de" : d.key
.
Fonction Les fonctions autonomes et les lambdas ont le même type d'objet et peuvent être transmises comme n'importe quel autre objet (voir ci-dessous). Mais seuls les lambdas peuvent avoir une liste de capture. Les fonctions régulières ne peuvent pas être exécutées pendant l'évaluation const, contrairement aux fonctions pure
. Les fonctions pures ne peuvent voir que les constantes et leurs arguments.
Exception Le seul type d'objets pouvant être lancés. Pour les créer, utilisez la fonction intégrée exception()
ou son raccourci ex()
.
Les instructions conditionnelles fonctionnent exactement comme en C
. La syntaxe est :
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
Et les accolades { }
peuvent être omises comme en C
, dans le cas de blocs à instruction unique. conditionExpr
peut être n'importe quelle expression, par exemple : (a=3)+b >= c && !d
.
Lorsque conditionExpr
est une expression qui peut être évaluée de manière constante, l'intégralité de l'instruction if est remplacée par la vraie branche, tandis que la fausse branche est ignorée. Par exemple, considérons le script suivant :
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
Non seulement il affiche toujours « oui », mais il n'a même pas besoin de vérifier quoi que ce soit avant de le faire. Vérifiez l'arbre de syntaxe abstraite :
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
prend en charge les boucles classiques while
et for
.
while ( condition ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
for ( var i = 0 ; i < 10 ; i += 1 ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
Ici, les accolades { }
peuvent être omises comme dans le cas ci-dessus. Il n'y a que quelques différences par rapport à C
qui méritent d'être soulignées :
Les opérateurs ++
et --
n'existent pas dans MyLang
pour le moment.
Pour déclarer plusieurs variables, utilisez la syntaxe : var a, b = [3,4];
ou simplement var a,b,c,d = 0;
si vous voulez que toutes les variables aient la même valeur initiale.
Pour augmenter la valeur de plusieurs variables, utilisez la syntaxe : a, b += [1, 2]
. Dans les cas extrêmement rares et complexes où, dans l'instruction d'incrémentation de la boucle for, nous devons attribuer à chaque variable une nouvelle variable en utilisant différentes expressions, profitez de la syntaxe d'expansion dans l'affectation : i, j = [i+2, my_next(i, j*3)]
.
MyLang
prend en charge les boucles foreach
en utilisant une syntaxe assez familière :
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Les boucles Foreach peuvent être utilisées pour les tableaux, les chaînes et les dictionnaires. Par exemple, parcourir chaque paire <key, value>
dans un dictionnaire est simple :
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
Pour parcourir uniquement chaque clé, utilisez simplement var k in d
à la place.
MyLang
prend également en charge l'énumération dans les boucles foreach. Vérifiez l'exemple suivant :
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
Autrement dit, lorsque le nom du conteneur est précédé du mot-clé indexed
, la première variable se voit attribuer un numéro progressif à chaque itération.
En parcourant un tableau de petits tableaux de taille fixe (pensez aux tuples), il est possible de développer directement ces "tuples" dans la boucle foreach :
var arr = [
[ " hello " , 42 ] ,
[ " world " , 11 ]
] ;
foreach ( var name , value in arr ) {
print ( name , value ) ;
}
# This is a shortcut for :
foreach ( var elem in arr ) {
# regular array expansion
var name , value = elem ;
print ( name , value ) ;
}
# Which is a shortcut for :
foreach ( var elem in arr ) {
var name = elem [ 0 ] ;
var value = elem [ 1 ] ;
print ( name , value ) ;
}
Déclarer une fonction est simple comme suit :
func add ( x , y ) {
return x + y ;
}
Mais plusieurs raccourcis sont également pris en charge. Par exemple, dans le cas de fonctions à instruction unique comme celle ci-dessus, la syntaxe suivante peut être utilisée :
func add ( x , y ) => x + y ;
De plus, même si c'est une bonne pratique de toujours écrire ()
pour les fonctions sans paramètres, elles sont en fait facultatives dans ce langage :
func do_something { print ( " hello " ) ; }
Les fonctions sont traitées comme des symboles réguliers dans MyLang
et il n'y a pas de différences substantielles entre les fonctions autonomes et les lambdas dans ce langage. Par exemple, nous pouvons déclarer la fonction add
(ci-dessus) comme lambda de cette façon :
var add = func ( x , y ) => x + y ;
Remarque : lors de la création d'objets fonction dans des expressions, nous ne sommes pas autorisés à leur attribuer un nom.
Les Lambda prennent également en charge une liste de capture, mais les captures implicites ne sont pas prises en charge, afin de garantir la clarté. Bien entendu, les lambdas peuvent être renvoyés comme n’importe quel autre objet. Par exemple:
func create_adder_func ( val ) =>
func [ val ] ( x ) => x + val ;
var f = create_adder_func ( 5 ) ;
print ( f ( 1 ) ) ; # Will print 6
print ( f ( 10 ) ) ; # Will print 15
Les lambdas avec captures ont un état , comme on pourrait s'y attendre. Considérez le script suivant :
func gen_counter ( val ) => func [ val ] {
val += 1 ;
return val ;
} ;
var c1 = gen_counter ( 5 ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
# Clone the `c1` lambda object as `c2` : now it will have
# its own state , indipendent from `c1` .
var c2 = clone ( c1 ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c2: " , c2 ( ) ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
Il génère la sortie :
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
Les objets de fonction réguliers définis par l'utilisateur (y compris les lambdas) ne sont pas considérés comme const
et, par conséquent, ne peuvent pas être exécutés pendant l'évaluation const. C'est une limitation assez forte. Prenons l'exemple suivant :
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
Dans ce cas, le script ne peut pas créer le tableau const sorted_people
. Parce que nous avons passé un objet fonction au module intégré const sort()
, nous obtiendrons une erreur ExpressionIsNotConstEx
. Bien sûr, si sorted_people
était déclaré comme var
, le script s'exécuterait, mais le tableau ne serait plus const et nous ne pourrons bénéficier d'aucune optimisation du temps d'analyse . Par conséquent, même si la fonction intégrée sort()
peut être appelée lors de l'évaluation const, lorsqu'elle dispose d'un paramètre compare func
personnalisé, cela n'est plus possible.
Pour surmonter la limitation qui vient d'être décrite, MyLang
dispose d'une syntaxe spéciale pour les fonctions pures . Lorsqu'une fonction est déclarée avec le mot-clé pure
précédant func
, l'interpréteur la traite d'une manière particulière : elle peut être appelée à tout moment, à la fois pendant l'évaluation const et pendant l'exécution mais la fonction ne peut pas voir les variables globales, ni capturer quoi que ce soit : elle ne peut utiliser constantes et la valeur de ses paramètres : c'est exactement ce dont nous avons besoin lors de l'évaluation de const. Par exemple, pour générer sorted_people
lors de l'évaluation const, il suffit d'écrire :
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
Les fonctions pures peuvent être définies comme des fonctions autonomes et peuvent également être utilisées avec des paramètres non const. Par conséquent, si une fonction peut être déclarée comme pure
, elle doit toujours être déclarée de cette façon. Par exemple, considérons le script suivant :
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
L'arbre de syntaxe abstraite que le moteur du langage utilisera au moment de l'exécution sera :
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
FuncDeclStmt(
Id("add2")
<NoCaptures>
IdList(
Id("x")
)
Expr04(
Id("x")
Op '+'
Int(2)
)
)
VarDecl(
Id("non_const")
Op '='
Int(25)
)
CallExpr(
Id("print")
ExprList(
CallExpr(
Id("add2")
ExprList(
Id("non_const")
)
)
)
)
CallExpr(
Id("print")
ExprList(
Int(7)
)
)
)
--------------------------
27
7
Comme vous pouvez le voir, dans le premier cas, un appel de fonction réel se produit car non_const
n'est pas une constante, tandis que dans le second cas, c'est COMME SI nous passions un entier littéral à print()
.
Comme pour les autres constructions, MyLang
a une gestion des exceptions similaire à celle de Python
, mais utilisant une syntaxe similaire à celle C++
. La construction de base est l'instruction try-catch
. Voyons un exemple :
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
Remarque : si une exception est générée par des expressions constantes (par exemple int("blah")
), lors de l'évaluation const, l'erreur sera signalée directement, en contournant toute logique de gestion des exceptions. La raison en est d'imposer un échec précoce .
Plusieurs instructions catch
sont également autorisées :
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
Et, dans le cas où plusieurs exceptions peuvent être gérées avec le même code, une syntaxe plus courte peut également être utilisée :
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
Les exceptions peuvent contenir des données, mais aucune des exceptions intégrées ne le fait actuellement. La liste des exceptions d'exécution intégrées qui peuvent être interceptées avec des blocs try-catch
est la suivante :
D’autres exceptions comme SyntaxErrorEx
ne peuvent pas être interceptées. Il est également possible dans MyLang
d'attraper N'IMPORTE QUELLE exception en utilisant un bloc catch-anything :
try {
# body
} catch {
# Something went wrong .
}
Ce langage ne prend pas en charge les types personnalisés pour le moment. Par conséquent, il n'est pas possible de lancer n'importe quel type d'objet comme dans d'autres langages. Pour lever une exception, il est nécessaire d'utiliser la fonction intégrée spéciale exception()
ou son raccourci, ex()
. Prenons l'exemple suivant :
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
Comme l'intuition le suggère, avec ex()
nous avons créé puis lancé un objet d'exception appelé MyError
, ayant 1234
comme données de charge utile. Plus tard, dans le bloc catch
, nous avons détecté l'exception et nous avons extrait les données utiles à l'aide de la fonction intégrée exdata()
.
Dans le cas où une exception donnée n'a pas besoin d'avoir une charge utile, il est possible de simplement sauvegarder le résultat de ex()
dans une variable et de le lancer plus tard en utilisant une syntaxe probablement plus agréable :
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
prend en charge la relance d'une exception dans le corps des instructions catch à l'aide du mot-clé rethrow
dédié :
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
Dans certains cas, il peut être nécessaire d'effectuer un nettoyage après l'exécution d'un bloc de code susceptible de lever une exception. Pour ces cas, MyLang
prend en charge la célèbre finally
, qui fonctionne exactement comme en C#
:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
Il convient de noter que les constructions try-finally
(sans aucune clause catch
) sont également autorisées.
Les fonctions intégrées suivantes seront évaluées pendant l'analyse lorsque les arguments const leur seront transmis.
defined(symbol)
Vérifiez si symbol
est défini. Renvoie 1 si le symbole est défini, 0 sinon.
len(container)
Renvoie le nombre d'éléments dans le conteneur donné.
str(value, [decimal_digits])
Convertissez la valeur donnée en chaîne. Si value
est un flottant, le 2ème paramètre indique le nombre souhaité de chiffres décimaux dans la chaîne de sortie.
int(value)
Convertit la chaîne donnée en entier. Si la valeur est flottante, elle sera truquée. Si la valeur est une chaîne, elle sera analysée et convertie en entier, si possible. Si la valeur est déjà un entier, elle sera renvoyée telle quelle.
float(value)
Convertissez la valeur donnée en float. Si la valeur est un entier, elle sera convertie en nombre à virgule flottante. Si la valeur est une chaîne, elle sera analysée et convertie en float, si possible. Si la valeur est déjà une valeur flottante, elle sera renvoyée telle quelle.
clone(obj)
Clonez l'objet donné. Utile pour les objets non triviaux tels que les tableaux, les dictionnaires et les lambda avec captures.
type(value)
Renvoie le nom du type de la valeur donnée sous forme de chaîne. Utile pour le débogage.
hash(value)
Renvoie la valeur de hachage utilisée par les dictionnaires en interne lorsque value
est utilisée comme clé. Pour le moment, seuls les entiers, les flottants et les chaînes prennent en charge hash()
.
array(N)
Renvoie un tableau de none