Bonjour à tous,
N’en déplaise à tous ceux qui diront « encore un », je commence à avoir un truc un minimum potable alors je me suis dit que c’était le moment de commencer à partager pour avoir des avis ou idées extérieures. J’ai pas d’idée de nom (je suis nul pour ça), du coup j’ai appelé mon langage QScript. Il est suceptible de changer de nom encore plusieurs fois (d’autant plus que QScript est sûrement déjà pris), mais pour le moment c’est pas très important.
Voici donc « encore » un mini-langage expérimental. IL est plus si mini que ça mais enfin bon…
Pour les gens pressés, le lien de téléchargement c’est en bas du post, mais je vous conseille quand même fortement de lire la suite sinon vous serez sûrement déçu, je ne prétends pas révolutionner le monde.
Concept et inspirations
On peut dire que mon mini-langage est une sorte de croisement entre JavaScript, Python et Ruby. Tout ça en même temps et en mieux évidemment. En bref le typage est faible et dynamique, bien qu’on déclare explicitement des classes comme en Java.
Mon langage est implémenté en C++14. Pour l’implémentation, je me suis pas mal inspiré de Wren 1, et un peu de lua 2.
Le parsing est fait à l’ancienne sans bibliothèque externe. Le code source est lu, transformé en structure arborescente, et le bytecode est généré ensuite à partir de là. Je ne sais pas si on peut vraiment parler d’AST par contre.
J’essaie dans la mesure du possible que la syntaxe reste à la fois agréable, souple et facile à parser.
J’aurais bien aimé me débarrasser du signe $
parce que c’est fondamentalement moche, mais je n’ai pas trouvé comment faire autrement sans compliquer énormément le parsing… c’est mon seul gros regret au niveau de la syntaxe. Sinon elle est assez classique, on pourrait dire que c’est du python sans obligation d’indenter, ou du JavaScript moins quelques parenthèses superflues. IL y a quelques extraits de qScript plus bas dans ce post qui vous donneront déjà une petite idée.
La VM fonctionne sur base d’une pile. Sur la pile d’un fil d’exécution, on place les variables locales, puis les valeurs temporaires utilisées pour les calculs. Quand on appelle une fonction, on décale la base de la pile et quand on la quitte, on restaure la base à son état précédent. Je me suis beaucoup inspiré de ce que faisait Wren pour la VM. Celle de lua est beaucoup plus compliquée (encore trop pour moi ) car c’est une machine à registres.
Coup d’oeil sur la syntaxe
Ca serait un peu long de tout détailler ici, je me contenterai d’un exemple commenté qui passe en revue un peu tout. Pour plus d’informations, vous pouvez lire [language-syntax.txt] qui explique tout: types de base, structures de contrôle, fonctions, programmation orientée objet, etc.
# Un exemple de classe
class Vector {
# Constructeur avec 3 paramètres comprenant des valeurs par défaut
constructor (x=0, y=0, z=0) {
_x = x # On définit implicitement un champ x
_y = y # on sait qu'on fait référence à un champ grâce au `_`
_z = z
}
# ON définit des accesseurs pour nos trois champs x, y et z
# Si la dernière instruction d'une méthode est une expression, alors le return est implicite
x { _x }
y { _y }
z { _z }
# On définit des mutateurs
x= (val) { _x=val }
y= (val) { _y=val }
z= (val) { _z = val }
# ON définit une méthode ordinaire
# Comme elle ne prend aucun paramètre, on peut omettre les parenthèses lors de son appel
# i.e. myvector.length et myvector.length() sont identiques
length { sqrt(_x**2 + _y**2 + _z**2) }
# Surcharge d'opérateur
# On n'a pas accès aux champs d'un autre objet (même s'il est censé être de la même classe), i.e. other._x est invalide; on doit passer par les accesseurs.
+ (other) { Vector(x+other.x, y+other.y, z+other.z) }
}
let vector1 = Vector(1, 2, 3)
let vector2 = Vector(3, 4)
let vector3 = vector1 + vector2 # ON appelle le + surchargé
print(vector2.length) #5
print(vector3.x) #4
print(vector3.y) #6
print(vector3.z) #3
# Un peu de listes et autres conteneurs
let tuple = ((1, "one"), (2, "two"), (3, "three"), (4, "four"))
let set = <1, 2, 3, 4, 5>
let list = [x**2 for x in set] # [1, 4, 9, 16, 25]
list.sort($(a,b): a%10<b%10) # [1, 4, 25, 16, 9]
list.sort(::>) # [25, 16, 9, 4, 1]
# Exemple de fonctions/lambda/closure bref appelez-les comme vous voulez
let g = $(start) {
return $(x) {
start+=1 # La variable start est prise dans la fermeture
return start*x
}}
let f1 = g(3), f2 = g(7)
print(f1(5)) #20
print(f1(6)) #30
print(f2(8)) #64
print(f2(10)) #90
# Exemple avec les fils d'exécution, un genre de générateur
let fibonacci = $*{
let a=0, b=1
while true {
let tmp=a
a+=b
b=tmp
yield a
}}
for var i in 0..10 {
# successivement 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
print(fibonacci())
}
# Encore des trucs avec les méthodes
let modulo = Num::% # En gros c'est la même chose qu'écrire $(a,b): a%b
print(modulo(10, 4)) #2
let tripler = 3::* # C'est un raccourci pour $x: 3*x
print(tripler(12)) #36
# Et ça, ça sert vraiment à rien, mais c'est rigolo
let a = 3, b = 4
Num::* = Num::/
print(a*b) #0.75 (!)
Performances
Je trouve que les benchmarks ne servent pas à grand chose, mais pour ceux que ça amuse, voici:
from time import clock
from math import sin
t = clock()
s = 0
for i in range(0, 10000000):
s+=sin(i)
t = clock() -t
print(t)
let t = clock()
let s = 0
for i in 0..10000000: s+=sin(i)
t = clock() -t
print(t)
Résultat chez moi: Python 2.71 vs. QScript 2.29. Voilà, je vous ai prévenu que c’était totalement inutile car absolument pas représentatif de scripts réels. D’autant plus que python est connu pour être lent sur les boucles.
Histoire et motivations
Mon but initial était d’ajouter du scripting pour un jeu. Comme tout le monde j’ai commencé par embarquer lua. C’est éprouvé, c’est ultra-rapide. Sauf que:
- Les tableaux commencent à 1, c’est une fonctionalité contre-intuitive qui n’apporte absolument aucun bénéfice
- C’est un langage objet orienté prototype, et c’est aussi très troublant (et vite compliqué) quand on n’a pas le mot-clé class
- Lua ne supporte pas l’UTF-8 (ou du moins n’apporte pas vraiment d’aide pour le gérer nativement), et j’y tiens quand même, parce que mon jeu est multilingue
- C’est assez personnel, mais je n’aime que moyennement la syntaxe; j’ai toujours largement préféré les langages à accolades plutôt que les langages à begin…end
Ayant atteint les limites de lua, j’ai tenté d’embarquer python 4. Python est un langage facile, populaire, et la bibliothèque stnandard est très fournie. L’embarquer dans un programme C++ est beaucoup plus complexe que lua, mais ça reste encore très jouable. Sauf que:
- Embarquer python c’est quand même assez gros, de l’ordre de 30 Mo au moins, même si on supprime tous les modules non indispensables. Non je ne m’en fiche pas royalement comme la plupart des gens, non mon jeu ne fait pas 10 Go, non je n’ai pas moi-même la fibre, et oui il y a des joueurs en Afrique qui ont des petites connexions (et ça j’ai des preuves).
- Pour l’embarquer vraiment bien, il faudrait recompiler python soi-même, ce que j’ai vite abandonné car c’est long et hyper compliqué. J’avais pris le python36.dll inclus dans la distribution standard pour windows, mais j’ai eu des conflits de MSVCRT (mon programme et le runtime python étaient linkées à des versions différentes de la MSVCRT et je n’ai pas pu tout résoudre)
- Mais surtout on a un énorme problème de sécurité, en cela qu’on ne peut pas totalement empêcher le scripteur d’utiliser la fonction open ou d’importer le module os car ils sont beaucoup trop implantés dans le coeur de python; il va sans dire que c’est un point critique pour un jeu, un utilisateur mal intentionné pouvant faire des dégâts chez le joueur innocent…
Après python j’ai pensé à JavaScript. Je trouve que JavaScript est devenu un chouette langage depuis ES6, ES2015 et ce qui a suivi. Ce qui est fourni en standard dans le langage suffit largement pour scripter un jeu. D’ailleurs sauf erreur unity propose JavaScript comme langage de script; malheureusement je n’utilise pas unity et je ne vais pas l’utiliser. De base si on prend vraiment V8 et pas Node.js, il n’y a pas de sockets, pas d’accès aux fichiers, etc. donc c’est totalement sûr de ce point de vue.
le code C++ d’exemple a l’air vraiment cool et pas si compliqué à embarquer. Sauf que… après avoir télécharger 4 Go de bouzin, je ne comprends toujours pas comment compiler leur m… C’est quoi ce délire avec perl, python 2.7, et des build tools qui sont nécessaires pour compiler d’autres build tools ? J’abandonne rapidement, avec l’intime conviction que Google a fait exprès d’obfusquer et compliquer son truc… oui monsieur, vous voyez, c’est libre… mais en fait c’est proprement incompréhensible et donc totalement inexploitable sauf à vous appeler Google vous-mêmes.
Toujours pour JavaScript, j’ai aussi évidemment trouvé SpeederMonkey et TraceMonkey. Mais là encore c’est compliqué. L’un des deux est considéré comme obsolète mais je n’ai pas vraiment compris lequel; j’ai pas trouvé beaucoup de doc non plus, ou alors ça avait l’air d’être un peu le foutoir sur la MDN. Et pourquoi est-ce qu’ils implémentent toujours ça en C ? Je croyais que firefox était écrit en C++ ? J’aimerais bien un truc en C++ quand même.
Du coup j’ai essayé de revenir à quelque chose de plus soft et plus spécialisé. J’ai découvert AngelScript 3, qui est ma foi plutôt bien foutu, et semble-til un temps soi peu déjà utilisé dans quelques projets. J’ai bossé un bon moment avec, j’ai même soumis un ou deux bugs à son auteur, ils ont été corrigés.
Là le problème que j’ai quand même trouvé, c’est que le concepteur laisse faire trop de choses soi-même. Les include, et même les strings et les tableaux sont à implémenter soi-même, et j’ai trouvé que les implémentations proposées en exemple étaient plutôt bof.
Bien que tout soit parfaitement prévu, la magouille pour avoir un système d’include et séparer son code dans plusieurs fichiers, c’est vraiment du gros bricolage.
Pour supporter les commentaires il faut même faire une fonction qui remplace ce qu’il y a entre /*
et */
avec des espaces. La bonne blague.
Ah, et puis finalement, un langage statiquement typé, c’est peut-être pas assez dynamique, pas assez flexible pour faire du scripting comme on l’entend habituellement ? En outre, plus j’utilise le langage, plus je sens qu’il y a un problème avec les handles. Je les trouve de plus en plus incohérents. Des fois on doit les utiliser, des fois on ne doit pas, des fois on ne peut pas; des fois ça se comporte comme en Java, des fois ça se comporte comme en C++. En plus des handles il existe toujours les références à la C++. Ca me parait totalement illogique d’avoir recours à un constructeur de copie ou un opérateur d’affectation dans certains cas alors qu’on travaille avec des handles ou des tableaux de handles… Au final on s’y perd.
Plus tard j’ai découvert Wren 1. IL est assez sympa ce petit langage; d’ailleurs je m’en suis beaucoup inspiré pour créer le mien. Je l’ai sérieusement considéré pendant un temps pour remplacer AngelScript, au point de le forker pour essayer des choses avec. Mais il y a malgré tout certaines choses qui me dérangent:
- C’est encore et toujours du C et rien que du C. En 2018 je trouve que c’est quand même dommage !
- Le code est compilé en une seule passe qui fait tout en même temps et rien n’est prévu pour enregistrer du bytecode. En clair ça signifie que les scripteurs qui publient leur travail sont forcément obligés de publier leur code source… pas forcément cool, tout le monde n’adhère pas à l’open source forcé; et en plus ça aide les tricheurs et pirates potentiels. Je n’ai pas encore implémenté l’enregistrement en bytecode moi-même mais je pense bien y arriver.
- La façon d’embarquer du code C natif est un peu étrange: on doit déclarer la classe et les méthodes en Wren, et retourner les fonctions dans des callbacks; c’est moyennement pratique pour mettre à disposition un grand nombre d’objets C/C++ en Wren. Vous verrez que de mon côté, je me suis bien amusé avec les templates pour faire en sorte que la mise à disposition d’API C++ en QScript soit assez facile.
- ET le plus gros problème, la réentrance n’est pas officiellement implémentée. En clair ça signifie qu’une méthode Wren implémentée en C ne peut pas elle-même appeler une méthode Wren par derrière. Par exemple, impossible d’implémenter une fonction de tri de liste en C qui ferait appel à une fonction de comparaison implémentée en Wren, ce qui oblige à refaire un algorithme de tri 100% en Wren. A ce stade je me dis que j’ai autre chose à faire que réinventer quicksort et que copier du code de quelqu’un d’autre en mode j’emplile les couches de scotch.
Alors voilà. Après ces quelques expériences plus ou moins mitigées, je me suis mis en tête que j’allais m’y essayer: concevoir mon petit langage de scripting à moi.
Au niveau des performances et de la simplicité d’utilisation, je suis probablement complètement à l’ouest, mais ça ne fait rien. Même si je ne parviens pas à mon but, j’aurai malgré tout appris énormément de choses sur les coulisses des langages qu’on utilise tous les jours; quoi qu’il arrive c’est une réussite. Au passage j’ai aussi appris bien des choses sur C++ lui-même parmi les plus avancées: utilisation des templates, les vtables, le RTTI, …
Merci de m’avoir lu jusqu’au bout.
Téléchargement
J’ai évidemment mis ça sur github, mais avant que vous n’alliez voir je préfère vous prévenir, le code n’est pas très propre ni très commenté. Ca fait partie des choses que je dois encore améliorer. Lien github: https://github.com/qtnc/qscript
Pour compiler vous-mêmes, vous aurez besoin de quelques-unes des bibliothèques de boost dont boost::regex, et utf8cpp. J’utilise boost::regex parce que std::regex n’existe visiblement pas chez moi avec MinGW, tout comme std::mutex, std::thread et d’autres par ailleurs.
Si vous ne voulez pas vous casser la tête à compiler vous-mêmes, vous trouverez un zip pour windows ici: http://vrac.quentinc.net/QScript.zip
Fonctionne en 32 et 64 bits. Je m’excuse d’avance auprès des linuxiens et des maqueux.