TL; DR
VKScript n'est pas JavaScript. La sémantique de ce langage est fondamentalement différente de la sémantique de JavaScript. Voir la conclusion .
Qu'est-ce que VKScript?
VKScript est un langage de programmation de script de type JavaScript utilisé dans la méthode API d' execute
VKontakte, qui permet aux clients de télécharger exactement les informations dont ils ont besoin. En substance, VKScript est un analogue de GraphQL utilisé par Facebook dans le même but.
Comparez GraphQL et VKScript:
Description de VKScript à partir de la page de méthode dans la documentation de l'API VK (la seule documentation en langue officielle):
Les éléments suivants sont pris en charge:
- opérations arithmétiques
- opérations logiques
- création de tableaux et de listes ([X, Y])
- parseInt et parseDouble
- concaténation (+)
- si construire
- filtre de tableau par paramètre (@.)
- Appels de méthode API , paramètre de longueur
- boucles utilisant l'instruction while
- Méthodes Javascript: tranche , push , pop , shift , unshift , splice , substr , split
- supprimer l' opérateur
- affectation aux éléments du tableau, par exemple: row.user.action = "test";
- recherchez dans un tableau ou une chaîne - indexOf , par exemple: «123» .indexOf (2) = 1, [1, 2, 3] .indexOf (3) = 2. Renvoie -1 si l'élément n'est pas trouvé.
La création de fonctions n'est actuellement pas prise en charge.
La documentation citée indique que "la compatibilité ECMAScript est prévue." Mais en est-il ainsi? Essayons de comprendre comment cette langue fonctionne de l'intérieur.
Table des matières
- Machine virtuelle VKScript
- Sémantique des objets VKScript
- Conclusion
Machine virtuelle VKScript
Comment analyser un programme en l'absence d'une copie locale? C'est vrai - envoyez des demandes au point de terminaison public et analysez les réponses. Essayons, par exemple, d'exécuter le code suivant:
while(1);
Nous obtenons une Runtime error occurred during code invocation: Too many operations
. Cela suggère que dans la mise en œuvre du langage, il y a une limite au nombre d'actions effectuées. Essayons de définir la valeur limite exacte:
var i = 0; while(i < 1000) i = i + 1;
Runtime error occurred during code invocation: Too many operations
.
var i = 0; while(i < 999) i = i + 1;
{"response": null}
- Le code a été exécuté avec succès.
Ainsi, la limite du nombre d'opérations est d'environ 1 000 cycles «inactifs». Mais, en même temps, il est clair qu'un tel cycle n'est probablement pas une opération «unitaire». Essayons de trouver une opération qui n'est pas divisée par le compilateur en plusieurs plus petites.
Le candidat le plus évident pour le rôle d'une telle opération est la soi-disant déclaration vide ( ;
). Cependant, après avoir ajouté au code avec i < 999
50 caractères ;
, la limite n'est pas dépassée. Cela signifie que soit l'instruction vide est levée par le compilateur et ne gaspille pas les opérations, soit une itération de la boucle prend plus de 50 opérations (ce qui n'est probablement pas le cas).
La prochaine chose qui me vient à l'esprit après ;
- calcul d'une expression simple (par exemple, comme ceci: 1;
). Essayons d'ajouter certaines de ces expressions à notre code:
var i = 0; while(i < 999) i = i + 1; 1;
Ainsi, 2 opérations 1;
dépenser plus d'opérations que 50 opérations ;
. Cela confirme l'hypothèse selon laquelle une instruction vide ne gaspille pas les instructions.
Essayons de réduire le nombre d'itérations du cycle et ajoutons 1;
supplémentaire 1;
. Il est facile de remarquer que pour chaque itération il y en a 5 supplémentaires 1;
par conséquent, une itération du cycle passe 5 fois plus d'opérations qu'une opération 1;
.
Mais y a-t-il une opération encore plus simple? Par exemple, l'ajout de l'opérateur unaire ~
ne nécessite pas le calcul d'expressions supplémentaires, et l'opération elle-même est effectuée sur le processeur. Il est logique de supposer que l'ajout de cette opération à l'expression augmente le nombre total d'opérations de 1.
Ajoutez cet opérateur à notre code:
var i = 0; while(i < 999) i = i + 1; ~1;
Et oui, nous pouvons ajouter un tel opérateur et une autre expression 1;
- plus. Par conséquent, 1;
n'est vraiment pas un opérateur unitaire.
Similaire à l'opérateur 1;
, nous allons réduire le nombre d'itérations de la boucle et ajouter les opérateurs ~
. Une itération s'est avérée équivalente à 10 opérations unitaires ~
, donc, l'expression 1;
passe 2 opérations.
Notez que la limite est d'environ 1000 itérations, soit environ 10000 opérations simples. Nous supposons que la limite est exactement de 10 000 opérations.
Mesurer le nombre d'opérations dans le code
Notez que nous pouvons maintenant mesurer le nombre d'opérations dans n'importe quel code. Pour ce faire, ajoutez ce code après la boucle et ajoutez / supprimez des itérations, des opérateurs ~
ou la dernière ligne entière, jusqu'à ce que l'erreur Too many operations
disparaisse.
Quelques résultats de mesure:
Déterminer le type de machine virtuelle
Vous devez d'abord comprendre le fonctionnement de l'interpréteur VKScript. Il existe deux options plus ou moins plausibles:
- L'interpréteur parcourt récursivement l'arborescence de syntaxe et effectue une opération sur chaque nœud.
- Le compilateur traduit l'arbre de syntaxe en une séquence d'instructions que l'interprète exécute.
Il est facile de comprendre que VKScript utilise la deuxième option. Considérez l'expression (true?1:1);
(5 opérations) et (false?1:1);
(4 opérations). Dans le cas de l'exécution séquentielle d'instructions, une opération supplémentaire s'explique par une transition qui «contourne» la mauvaise option, et dans le cas d'un bypass AST récursif, les deux options sont équivalentes pour l'interpréteur. Un effet similaire est observé dans if / else avec une condition différente.
Il convient également de prêter attention à la paire i = 1;
(3 opérations) et var j = 1;
(1 opération). La création d'une nouvelle variable ne coûte qu'une opération et l'affectation à une existante coûte 3? Le fait que la création d'une opération à coût variable 1 (et, très probablement, soit une opération de chargement constant), dit deux choses:
- Lors de la création d'une nouvelle variable, il n'y a pas d'allocation de mémoire explicite pour la variable.
- Lors de la création d'une nouvelle variable, la valeur n'est pas chargée dans la cellule mémoire. Cela signifie que l'espace pour la nouvelle variable est alloué là où la valeur de l'expression a été calculée, et ensuite cette mémoire est considérée comme allouée. Cela suggère l'utilisation d'une machine à empiler.
L'utilisation de la pile explique également que l'expression var j = 1;
s'exécute plus rapidement que l'expression 1;
: la dernière expression dépense des instructions supplémentaires pour supprimer la valeur calculée de la pile.
Déterminer la valeur limite exacte
Notez que le cycle var j=0;while(j < 1)j=j+1;
(15 opérations) est une petite copie du cycle qui a été utilisé pour les mesures:
Arrête quoi? Y a-t-il une limite de 9998 instructions? Il nous manque clairement quelque chose ...
Notez que le code return 1;
est return 1;
effectué selon les mesures pour 0 instructions. Cela s'explique facilement: le compilateur ajoute un return null;
implicite return null;
à la fin du code return null;
, et lors de l'ajout de son retour, il échoue. En supposant que la limite est de 10000, nous concluons que l'opération return null;
prend 2 instructions (c'est probablement quelque chose comme push null; return;
).
Blocs de code imbriqués
Prenons quelques mesures supplémentaires:
Prenons attention aux faits suivants:
- L'ajout d'une variable à un bloc nécessite une opération supplémentaire.
- Lors de la "déclaration à nouveau d'une variable", la deuxième déclaration est remplie comme une affectation normale.
- Mais en même temps, la variable à l'intérieur du bloc n'est pas visible de l'extérieur (voir le dernier exemple).
Il est facile de comprendre qu'une opération supplémentaire est dépensée pour supprimer de la pile les variables locales déclarées dans le bloc. Par conséquent, lorsqu'il n'y a pas de variables locales, rien ne doit être supprimé.
Objets, méthodes, appels API
Analysons les résultats. Vous remarquerez peut-être que la création d'une chaîne et d'un tableau / objet vide nécessite 2 opérations, tout comme le chargement d'un nombre. Lors de la création d'un tableau ou d'un objet non vide, les opérations consacrées au chargement des éléments du tableau / objet sont ajoutées. Cela suggère que la création directe d'un objet se produit en une seule opération. Dans le même temps, aucun temps n'est perdu pour télécharger les noms de propriété; par conséquent, leur téléchargement fait partie de l'opération de création de l'objet.
Avec l'appel de méthode API, tout est également assez courant - charger une unité, appeler la méthode, pop
résultat (vous pouvez remarquer que le nom de la méthode est traité dans son ensemble, et non comme prenant des propriétés). Mais les trois derniers exemples semblent intéressants.
"".substr(0, 0);
- chargement d'une chaîne, chargement zéro, chargement zéro, résultat pop
. Pour une raison, il existe 2 instructions pour appeler une méthode (pour une raison quelconque, voir ci-dessous).var j={};jx=1;
- création d'un objet, chargement d'un objet, chargement d'une unité, pop
unité après affectation. Encore une fois, il y a 2 instructions pour l'affectation.var j={x:1};delete jx;
- chargement d'une unité, création d'un objet, chargement d'un objet, suppression. Il y a 3 instructions par opération de suppression.
Sémantique des objets VKScript
Les chiffres
Retour à la question d'origine: VKScript est-il un sous-ensemble de JavaScript ou d'un autre langage? Faisons un test simple:
return 1000000000 + 2000000000;
{"response": -1294967296};
Comme nous pouvons le voir, l'addition d'entiers conduit à un débordement, malgré le fait que JavaScript n'a pas d'entiers en tant que tels. Il est également facile de vérifier que la division par 0 entraîne une erreur et ne renvoie pas Infinity
.
Les objets
return {};
{"response": []}
Arrête quoi? Nous retournons un objet et obtenons un tableau ? Oui, ça l'est. Dans VKScript, les tableaux et les objets sont représentés par le même type, en particulier, un objet vide et un tableau vide sont une seule et même chose. Dans ce cas, la propriété length
de l'objet fonctionne et renvoie le nombre de propriétés.
Il est intéressant de voir comment les méthodes de liste se comportent si vous les appelez sur un objet?
return {a:1, b:2, c:3}.pop();
3
La méthode pop
renvoie la dernière propriété déclarée, qui est cependant logique. Modifiez l'ordre des propriétés:
return {b:1, c:2, a:3}.pop();
3
Apparemment, les objets dans VKScript se souviennent de l'ordre dans lequel les propriétés sont attribuées. Essayons d'utiliser des propriétés numériques:
return {'2':1,'1':2,'0':3}.pop();
3
Voyons maintenant comment fonctionne push:
var a = {'2':'a','1':'b','x':'c'}; a.push('d'); return a;
{"1": "b", "2": "a", "3": "d", "x": "c"};
Comme vous pouvez le voir, la méthode push trie les touches numériques et ajoute une nouvelle valeur après la dernière touche numérique. Les «trous» ne sont pas remplis dans ce cas.
Essayez maintenant de combiner ces deux méthodes:
var a = {'2':'a','1':'b','x':'c'}; a.push(a.pop()); return a;
{"1": "b", "2": "a", "3": "c", "x": "c"};
Comme nous le voyons, l'élément n'a pas été supprimé du tableau. Cependant, si nous mettons push
and pop
sur différentes lignes, le bug disparaîtra. Nous devons aller plus loin!
Stockage d'objets
var x = {}; var y = x; xy = 'z'; return y;
{"response": []}
Il s'est avéré que les objets en VKScript sont stockés par valeur, contrairement à JavaScript. Nous voyons maintenant l'étrange comportement de la chaîne a.push(a.pop());
- apparemment, l'ancienne valeur du tableau a été enregistrée sur la pile, d'où elle a été prise plus tard.
Cependant, comment les données sont-elles alors stockées dans l'objet si la méthode le modifie? Apparemment, l'instruction «extra» lors de l'appel de la méthode est conçue spécifiquement pour réécrire les modifications sur l'objet.
Méthodes de tableau
Conclusion
VKScript n'est pas JavaScript. Contrairement à JavaScript, les objets qu'il contient sont stockés par valeur, et non par référence, et ont une sémantique complètement différente. Cependant, lors de l'utilisation de VKScript pour l'usage auquel il est destiné, la différence n'est pas perceptible.
PS Sémantique des opérateurs
Les commentaires mentionnaient la combinaison d'objets via +
. À cet égard, j'ai décidé d'ajouter des informations sur le travail des opérateurs.
Pour les opérations avec des nombres (sauf pour les bits), si les opérandes sont int
et double
, int
est double
en double
. Si les deux opérandes sont int
, une opération est effectuée sur des entiers 32 bits signés (avec débordement).