VKScript-Sprachanalyse: JavaScript, oder?

TL; DR




VKScript ist kein JavaScript. Die Semantik dieser Sprache unterscheidet sich grundlegend von der Semantik von JavaScript. Siehe die Schlussfolgerung .


Was ist VKScript?




VKScript ist eine JavaScript-ähnliche Skriptprogrammiersprache, die in der VKontakte execute API-Methode verwendet wird, mit der Kunden genau die Informationen herunterladen können, die sie benötigen. Im Wesentlichen ist VKScript ein Analogon zu GraphQL, das von Facebook für denselben Zweck verwendet wird.


Vergleichen Sie GraphQL und VKScript:


GraphQLVKScript
ImplementierungenViele Open-Source-Implementierungen in verschiedenen ProgrammiersprachenDie einzige Implementierung innerhalb der VK-API
Basierend aufBrandneue SpracheJavascript
Die MöglichkeitenDatenanforderung, begrenzte Filterung; Abfrageargumente können die Ergebnisse früherer Abfragen nicht verwendenNachbearbeitung von Daten nach Ermessen des Kunden; API-Anforderungen werden in Form von Methoden dargestellt und können beliebige Daten aus früheren Anforderungen verwenden

Beschreibung von VKScript auf der Methodenseite in der VK-API-Dokumentation (die einzige offizielle Sprachdokumentation):


CodeAlgorithmuscode in VKScript - ein Format ähnlich JavaScript oder ActionScript (Kompatibilität mit ECMAScript wird vorausgesetzt) . Der Algorithmus sollte mit dem Befehl return% expression% enden. Operatoren müssen durch Semikolons getrennt werden.
Zeichenfolge

Folgendes wird unterstützt:


  • arithmetische Operationen
  • logische Operationen
  • Erstellung von Arrays und Listen ([X, Y])
  • parseInt und parseDouble
  • Verkettung (+)
  • wenn konstruieren
  • Array-Filter nach Parameter (@.)
  • API- Methodenaufrufe, Längenparameter
  • Schleifen mit der while- Anweisung
  • Javascript-Methoden: Slice , Push , Pop , Shift , Unshift , Splice , Substr , Split
  • Operator löschen
  • Zuordnung zu Array-Elementen, zum Beispiel: row.user.action = "test";
  • Die Suche im Array oder String lautet indexOf , zum Beispiel: "123" .indexOf (2) = 1, [1, 2, 3] .indexOf (3) = 2. Gibt -1 zurück, wenn das Element nicht gefunden wird.

Die Funktionserstellung wird derzeit nicht unterstützt.



In der zitierten Dokumentation heißt es: "ECMAScript-Kompatibilität ist geplant." Aber ist das so? Versuchen wir herauszufinden, wie diese Sprache von innen funktioniert.



Inhalt




  1. Virtuelle VKScript-Maschine
  2. Semantik von VKScript-Objekten
  3. Fazit

Virtuelle VKScript-Maschine




Wie kann ein Programm ohne lokale Kopie analysiert werden? Das ist richtig - senden Sie Anfragen an den öffentlichen Endpunkt und analysieren Sie die Antworten. Versuchen wir beispielsweise, den folgenden Code auszuführen:



 while(1); 

Beim Runtime error occurred during code invocation: Too many operations ein Runtime error occurred during code invocation: Too many operations . Dies deutet darauf hin, dass bei der Implementierung der Sprache die Anzahl der durchgeführten Aktionen begrenzt ist. Versuchen wir, den genauen Grenzwert festzulegen:


 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} - Der Code wurde erfolgreich ausgeführt.

Somit liegt die Grenze für die Anzahl der Operationen bei etwa 1000 "Leerlauf" -Zyklen. Gleichzeitig ist jedoch klar, dass ein solcher Zyklus höchstwahrscheinlich keine „einheitliche“ Operation ist. Versuchen wir, eine Operation zu finden, die vom Compiler nicht in mehrere kleinere unterteilt wird.


Der offensichtlichste Kandidat für die Rolle einer solchen Operation ist die sogenannte leere Anweisung ( ; ). Nach dem Hinzufügen zum Code mit i < 999 50 Zeichen ; wird das Limit nicht überschritten. Dies bedeutet, dass entweder die leere Anweisung vom Compiler ausgelöst wird und keine Operationen verschwendet, oder dass eine Iteration der Schleife mehr als 50 Operationen erfordert (was höchstwahrscheinlich nicht der Fall ist).


Das nächste, was mir danach einfällt ; - Berechnung eines einfachen Ausdrucks (zum Beispiel wie folgt: 1; ). Versuchen wir, einige dieser Ausdrücke zu unserem Code hinzuzufügen:


 var i = 0; while(i < 999) i = i + 1; 1; //    1; //       "Too many operations" 

Somit sind 2 Operationen 1; mehr Operationen als 50 Operationen ausgeben ; . Dies bestätigt die Hypothese, dass eine leere Anweisung keine Anweisungen verschwendet.


Versuchen wir, die Anzahl der Iterationen des Zyklus zu verringern und eine zusätzliche 1; hinzuzufügen 1; . Es ist leicht zu bemerken, dass es für jede Iteration 5 zusätzliche 1; Daher verbraucht eine Iteration des Zyklus fünfmal mehr Operationen als eine Operation 1; .


Aber gibt es eine noch einfachere Bedienung? Zum Hinzufügen des unären Operators ~ ist beispielsweise keine Berechnung zusätzlicher Ausdrücke erforderlich, und die Operation selbst wird auf dem Prozessor ausgeführt. Es ist logisch anzunehmen, dass das Hinzufügen dieser Operation zum Ausdruck die Gesamtzahl der Operationen um 1 erhöht.


Fügen Sie diesen Operator unserem Code hinzu:


 var i = 0; while(i < 999) i = i + 1; ~1; 

Und ja, wir können einen solchen Operator und einen weiteren Ausdruck 1; hinzufügen 1; - nicht mehr. Daher 1; ist wirklich kein einheitlicher Operator.


Ähnlich wie Operator 1; reduzieren wir die Anzahl der Iterationen der Schleife und fügen die Operatoren ~ . Eine Iteration entsprach 10 einheitlichen Operationen ~ , daher Ausdruck 1; verbringt 2 Operationen.


Es ist zu beachten, dass die Grenze ungefähr 1000 Iterationen beträgt, d. H. Ungefähr 10.000 Einzeloperationen. Wir gehen davon aus, dass das Limit genau 10.000 Operationen beträgt.



Messen der Anzahl von Operationen im Code




Beachten Sie, dass wir jetzt die Anzahl der Operationen in jedem Code messen können. Fügen Sie dazu diesen Code nach der Schleife hinzu und fügen Sie Iterationen, ~ -Operatoren oder die gesamte letzte Zeile hinzu / entfernen Sie sie, bis der Fehler Too many operations verschwindet.


Einige Messergebnisse:


CodeAnzahl der Operationen
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


Bestimmen des Typs der virtuellen Maschine




Zuerst müssen Sie verstehen, wie der VKScript-Interpreter funktioniert. Es gibt zwei mehr oder weniger plausible Optionen:


  • Der Interpreter durchläuft rekursiv den Syntaxbaum und führt eine Operation für jeden Knoten aus.
  • Der Compiler übersetzt den Syntaxbaum in eine Folge von Anweisungen, die der Interpreter ausführt.

Es ist leicht zu verstehen, dass VKScript die zweite Option verwendet. Betrachten Sie den Ausdruck (true?1:1); (5 Operationen) und (false?1:1); (4 Operationen). Im Fall der sequentiellen Ausführung von Anweisungen wird eine zusätzliche Operation durch einen Übergang erklärt, der die falsche Option „umgeht“, und im Fall einer rekursiven AST-Umgehung sind beide Optionen für den Interpreter äquivalent. Ein ähnlicher Effekt wird bei if / else mit einem anderen Zustand beobachtet.


Es lohnt sich auch, auf das Paar i = 1; zu achten i = 1; (3 Operationen) und var j = 1; (1 Operation). Das Erstellen einer neuen Variablen kostet nur 1 Operation, und das Zuweisen zu einer vorhandenen kostet 3? Die Tatsache, dass das Erstellen einer Variablen 1 Operation kostet (und höchstwahrscheinlich eine konstante Ladeoperation ist), sagt zwei Dinge aus:


  • Beim Erstellen einer neuen Variablen gibt es keine explizite Speicherzuordnung für die Variable.
  • Beim Erstellen einer neuen Variablen wird der Wert nicht in die Speicherzelle geladen. Dies bedeutet, dass der Speicherplatz für die neue Variable dort zugewiesen wird, wo der Wert des Ausdrucks berechnet wurde, und danach dieser Speicher als zugewiesen betrachtet wird. Dies legt die Verwendung einer Stapelmaschine nahe.

Die Verwendung des Stapels erklärt auch, dass der Ausdruck var j = 1; läuft schneller als Ausdruck 1; : Der letzte Ausdruck gibt zusätzliche Anweisungen zum Entfernen des berechneten Werts aus dem Stapel aus.



Ermittlung des genauen Grenzwertes


Man beachte, dass der Zyklus var j=0;while(j < 1)j=j+1; (15 Operationen) ist eine kleine Kopie des Zyklus, der für Messungen verwendet wurde:


CodeAnzahl der Operationen
 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; 

(Grenze)
9998

Hör auf was? Gibt es ein Limit von 9998 Anweisungen? Uns fehlt eindeutig etwas ...


Beachten Sie, dass der return 1; durchgeführt nach Messungen für 0 Anweisungen. Dies ist leicht zu erklären: Der Compiler fügt am Ende des Codes eine implizite return null; hinzu return null; und wenn die Rückgabe hinzugefügt wird, schlägt dies fehl. Unter der Annahme, dass das Limit 10000 beträgt, schließen wir, dass die Operation return null; nimmt 2 Anweisungen (wahrscheinlich ist dies so etwas wie push null; return; ).



Verschachtelte Codeblöcke




Nehmen wir noch einige Messungen vor:


CodeAnzahl der Operationen
{};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

Beachten wir die folgenden Fakten:


  • Das Hinzufügen einer Variablen zu einem Block erfordert eine zusätzliche Operation.
  • Beim "erneuten Deklarieren einer Variablen" erfüllt die zweite Deklaration eine normale Zuordnung.
  • Gleichzeitig ist die Variable innerhalb des Blocks von außen nicht sichtbar (siehe letztes Beispiel).

Es ist leicht zu verstehen, dass eine zusätzliche Operation zum Entfernen der im Block deklarierten lokalen Variablen vom Stapel aufgewendet wird. Dementsprechend muss nichts gelöscht werden, wenn keine lokalen Variablen vorhanden sind.



Objekte, Methoden, API-Aufrufe




CodeAnzahl der Operationen
"";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

Lassen Sie uns die Ergebnisse analysieren. Möglicherweise stellen Sie fest, dass das Erstellen einer Zeichenfolge und eines leeren Arrays / Objekts zwei Vorgänge erfordert, ebenso wie das Laden einer Zahl. Beim Erstellen eines nicht leeren Arrays oder Objekts werden Vorgänge zum Laden von Elementen des Arrays / Objekts hinzugefügt. Dies deutet darauf hin, dass das direkte Erstellen eines Objekts in einer Operation erfolgt. Gleichzeitig wird keine Zeit mit dem Herunterladen von Eigenschaftsnamen verschwendet. Daher ist das Herunterladen Teil des Vorgangs zum Erstellen des Objekts.


Mit dem API-Methodenaufruf ist auch alles ganz normal: Laden einer Einheit, tatsächliches Aufrufen der Methode, pop Ergebnisses (Sie können feststellen, dass der Methodenname als Ganzes verarbeitet wird und keine Eigenschaften annimmt). Aber die letzten drei Beispiele sehen interessant aus.


  • "".substr(0, 0); - Laden eines Strings, Laden von Null, Laden von Null, pop Ergebnis. Aus einem bestimmten Grund gibt es zwei Anweisungen zum Aufrufen einer Methode (aus irgendeinem Grund siehe unten).
  • var j={};jx=1; - Erstellen eines Objekts, Laden eines Objekts, Laden einer Einheit, pop Einheit nach Zuweisung. Auch hier gibt es 2 Anweisungen für die Zuordnung.
  • var j={x:1};delete jx; - Laden einer Einheit, Erstellen eines Objekts, Laden eines Objekts, Löschen. Es gibt 3 Anweisungen pro Löschvorgang.



Semantik von VKScript-Objekten


Die Zahlen




Zurück zur ursprünglichen Frage: Ist VKScript eine Teilmenge von JavaScript oder einer anderen Sprache? Lassen Sie uns einen einfachen Test machen:


 return 1000000000 + 2000000000; 

 {"response": -1294967296}; 

Wie wir sehen können, führt das Hinzufügen von Ganzzahlen zu einem Überlauf, obwohl JavaScript keine Ganzzahlen als solche hat. Es ist auch leicht zu überprüfen, ob das Teilen durch 0 zu einem Fehler führt und keine Infinity .



Die Objekte




 return {}; 

 {"response": []} 

Hör auf was? Wir geben ein Objekt zurück und erhalten ein Array ? Ja, das ist es. In VKScript werden Arrays und Objekte vom selben Typ dargestellt, insbesondere sind ein leeres Objekt und ein leeres Array ein und dasselbe. In diesem Fall funktioniert die length Eigenschaft des Objekts und gibt die Anzahl der Eigenschaften zurück.


Es ist interessant zu sehen, wie sich Listenmethoden verhalten, wenn Sie sie für ein Objekt aufrufen.


 return {a:1, b:2, c:3}.pop(); 

 3 

Die pop Methode gibt die zuletzt deklarierte Eigenschaft zurück, was jedoch logisch ist. Ändern Sie die Reihenfolge der Eigenschaften:


 return {b:1, c:2, a:3}.pop(); 

 3 

Anscheinend erinnern sich Objekte in VKScript an die Reihenfolge, in der Eigenschaften zugewiesen werden. Versuchen wir, numerische Eigenschaften zu verwenden:


 return {'2':1,'1':2,'0':3}.pop(); 

 3 

Nun wollen wir sehen, wie Push funktioniert:


 var a = {'2':'a','1':'b','x':'c'}; a.push('d'); return a; 

 {"1": "b", "2": "a", "3": "d", "x": "c"}; 

Wie Sie sehen können, sortiert die Push-Methode die Zifferntasten und fügt nach der letzten Zifferntaste einen neuen Wert hinzu. "Löcher" werden in diesem Fall nicht gefüllt.


Versuchen Sie nun, diese beiden Methoden zu kombinieren:


 var a = {'2':'a','1':'b','x':'c'}; a.push(a.pop()); return a; 

 {"1": "b", "2": "a", "3": "c", "x": "c"}; 

Wie wir sehen, wurde das Element nicht aus dem Array gelöscht. Wenn wir jedoch push und pop in verschiedene Zeilen setzen, verschwindet der Fehler. Wir müssen tiefer gehen!



Objektspeicherung




 var x = {}; var y = x; xy = 'z'; return y; 

 {"response": []} 

Wie sich herausstellte, werden Objekte in VKScript im Gegensatz zu JavaScript nach Wert gespeichert. Jetzt sehen wir das seltsame Verhalten der Zeichenfolge a.push(a.pop()); - Anscheinend wurde der alte Wert des Arrays auf dem Stapel gespeichert, von wo er später übernommen wurde.


Wie werden dann jedoch die Daten im Objekt gespeichert, wenn die Methode sie ändert? Anscheinend ist die "zusätzliche" Anweisung beim Aufrufen der Methode speziell zum Zurückschreiben von Änderungen an das Objekt konzipiert.



Array-Methoden




MethodeAktion
push
  • Ziffern nach Wert sortieren
  • Nehmen Sie den maximalen numerischen Schlüssel und fügen Sie einen hinzu
  • schreibe Argument in Array
  • Fügen Sie am Ende des Arrays nicht numerische Schlüssel hinzu
popEntfernen Sie das letzte Element aus dem Array (nicht unbedingt mit einem numerischen Schlüssel) und kehren Sie zurück.
der Rest
  • Sortieren Sie die Zifferntasten nach Wert und entfernen Sie die „Löcher“ im Array
  • Führen Sie eine entsprechende Javascript-Operation durch
  • Fügen Sie am Ende des Arrays nicht numerische Schlüssel hinzu

Bei Verwendung der Slice-Methode werden Änderungen nicht gespeichert



Fazit




VKScript ist kein JavaScript. Im Gegensatz zu JavaScript werden darin enthaltene Objekte nach Wert und nicht nach Referenz gespeichert und haben eine völlig andere Semantik. Wenn Sie VKScript jedoch für den Zweck verwenden, für den es bestimmt ist, ist der Unterschied nicht erkennbar.



PS Semantik von Operatoren




In den Kommentaren wurde das Kombinieren von Objekten über + . In diesem Zusammenhang habe ich beschlossen, Informationen über die Arbeit der Betreiber hinzuzufügen.


BetreiberAktionen
+
  • Wenn beide Argumente Objekte sind, erstellen Sie eine Kopie des ersten Objekts und fügen Sie die Schlüssel des zweiten (mit Ersetzung) hinzu.
  • Wenn beide Argumente Zahlen sind, fügen Sie sie als Zahlen hinzu.
  • Andernfalls werden beide Operanden in eine Zeichenfolge umgewandelt und als Zeichenfolgen hinzugefügt.
Andere arithmetische OperatorenBeide Operanden werden in eine Zahl umgewandelt, und die entsprechende Operation wird ausgeführt. Für Bitoperationen werden Operanden zusätzlich in int .
VergleichsoperatorenWenn zwei Zeichenfolgen oder zwei Zahlen verglichen werden, werden sie direkt verglichen. Wenn eine Zeichenfolge und eine Zahl verglichen werden und die Zeichenfolge eine korrekte Notation für die Zahl ist, wird die Zeichenfolge in eine Zahl umgewandelt. Andernfalls wird ein Fehler beim Comparing values of different or unsupported types zurückgegeben.
Auf Schnur werfenZahlen und Zeichenfolgen werden wie in JavaScript angegeben. Objekte werden als durch Kommas getrennte Werteliste in der Reihenfolge der Schlüssel aufgelistet. false und null werden als "" , true als "1" .
Besetzung zuWenn das Argument eine Zeichenfolge ist, die eine gültige Notation ist, wird die Nummer zurückgegeben. Andernfalls wird ein Numeric arguments expected Fehler mit Numeric arguments expected zurückgegeben.

Bei Operationen mit Zahlen (außer Bit) wird int double , wenn die Operanden int und double sind. Wenn beide Operanden int , wird eine Operation für vorzeichenbehaftete 32-Bit-Ganzzahlen (mit Überlauf) ausgeführt.

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


All Articles