Analyse du langage VKScript: JavaScript, êtes-vous?

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:


GraphQLVKScript
ImplémentationsDe nombreuses implémentations open source dans différents langages de programmationLa seule implémentation au sein de l'API VK
Basé surNouvelle langueJavascript
Les possibilitésDemande de données, filtrage limité; les arguments de requête ne peuvent pas utiliser les résultats des requêtes précédentesTout post-traitement de données à la discrétion du client; Les requêtes API sont présentées sous forme de méthodes et peuvent utiliser toutes les données des requêtes précédentes

Description de VKScript à partir de la page de méthode dans la documentation de l'API VK (la seule documentation en langue officielle):


codecode d'algorithme en VKScript - un format similaire à JavaScript ou ActionScript (la compatibilité avec ECMAScript est supposée) . L'algorithme doit se terminer par la commande return% expression% . Les opérateurs doivent être séparés par des points-virgules.
chaîne

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




  1. Machine virtuelle VKScript
  2. Sémantique des objets VKScript
  3. 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; //    1; //       "Too many operations" 

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:


CodeNombre d'opérations
1;2
~1;3
1+1;4
1+1+1;6
(true?1:1);5
(false?1:1);4
if(0)1;2
if(1)1;4
if(0)1;else 1;4
if(1)1;else 1;5
while(0);2
i=1;3
i=i+1;5
var j = 1;1
var j = 0;while(j < 1)j=j+1;15


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:


CodeNombre d'opérations
 var i = 0; while(i < 1) i = i + 1; 
15
 var i = 0; while(i < 999) i = i + 1; 
15 + 998 * 10 = 9995
 var i = 0; while(i < 999) i = i + 1; ~1; 

(limite)
9998

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:


CodeNombre d'opérations
{};0
{var j = 1;};2
{var j = 1, k = 2;};3
{var j = 1; var k = 2;};3
var j = 1; var j = 1;4
{var j = 1;}; var j = 1;3

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




CodeNombre d'opérations
"";2
"abcdef";2
{};2
[];2
[1, 2, 3];5
{a: 1, b: 2, c: 3};5
API.users.isAppUser(1);3
"".substr(0, 0);6
var j={};jx=1;6
var j={x:1};delete jx;6

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




La méthodeAction
push
  • trier les clés numériques par valeur
  • prendre la clé numérique maximale, en ajouter une
  • écrire l'argument dans le tableau
  • ajouter des clés non numériques à la fin du tableau
popSupprimez le dernier élément du tableau (pas nécessairement avec une touche numérique) et revenez.
le reste
  • trier les clés numériques par valeur, supprimer les «trous» dans le tableau
  • effectuer une opération javascript appropriée
  • ajouter des clés non numériques à la fin du tableau

Lors de l'utilisation de la méthode de tranche, les modifications ne sont pas enregistrées



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.


OpératriceActions
+
  • Si les deux arguments sont des objets, créez une copie du premier objet et ajoutez-y les clés du second (avec remplacement).
  • Si les deux arguments sont des nombres, ajoutez en tant que nombres.
  • Sinon, les deux opérandes sont convertis en chaîne et ajoutés en tant que chaînes.
Autres opérateurs arithmétiquesLes deux opérandes sont convertis en un nombre et l'opération correspondante est effectuée. Pour les opérations sur les bits, les opérandes sont également convertis en int .
Opérateurs de comparaisonSi deux chaînes ou deux nombres sont comparés, ils sont comparés directement. Si une chaîne et un nombre sont comparés et que la chaîne est une notation correcte pour le nombre, la chaîne est convertie en un nombre. Sinon, une erreur de Comparing values of different or unsupported types est renvoyée.
Cast to stringLes nombres et les chaînes sont donnés comme en JavaScript. Les objets sont répertoriés sous la forme d'une liste de valeurs séparées par des virgules dans l'ordre des clés. false et null sont castés comme "" , true casté comme "1" .
Diffuser versSi l'argument est une chaîne qui est une notation numérique valide, le nombre est renvoyé. Sinon, une erreur Numeric arguments expected est renvoyée.

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).

Source: https://habr.com/ru/post/fr464099/


All Articles