Analisis Bahasa VKScript: JavaScript, kan?

TL; DR




VKScript bukan JavaScript. Semantik bahasa ini pada dasarnya berbeda dari semantik JavaScript. Lihat kesimpulannya .


Apa itu VKScript?




VKScript adalah bahasa pemrograman scripting JavaScript-seperti yang digunakan dalam VKontakte execute metode API, yang memungkinkan pelanggan mengunduh persis informasi yang mereka butuhkan. Intinya, VKScript adalah analog GraphQL yang digunakan oleh Facebook untuk tujuan yang sama.


Bandingkan GraphQL dan VKScript:


GraphQLVKScript
ImplementasiBanyak implementasi open-source dalam berbagai bahasa pemrogramanSatu-satunya implementasi dalam VK API
BerdasarkanBahasa baruJavascript
KemungkinanPermintaan data, pemfilteran terbatas; argumen kueri tidak dapat menggunakan hasil dari kueri sebelumnyaSetiap post-processing data berdasarkan kebijaksanaan klien; Permintaan API disajikan dalam bentuk metode dan dapat menggunakan data apa pun dari permintaan sebelumnya

Deskripsi VKScript dari halaman metode dalam dokumentasi API VK (satu-satunya dokumentasi bahasa resmi):


kodekode algoritma dalam VKScript - format yang mirip dengan JavaScript atau ActionScript (diasumsikan kompatibel dengan ECMAScript ) . Algoritme harus diakhiri dengan perintah return% expression% . Operator harus dipisahkan dengan titik koma.
tali

Yang berikut ini didukung:


  • operasi aritmatika
  • operasi logis
  • pembuatan array dan daftar ([X, Y])
  • parseInt dan parseDouble
  • penggabungan (+)
  • jika membangun
  • filter array dengan parameter (@.)
  • Panggilan metode API , parameter panjang
  • loop menggunakan pernyataan while
  • Metode Javascript: slice , push , pop , shift , unshift , splice , substr , split
  • hapus operator
  • tugas untuk elemen array, misalnya: row.user.action = "test";
  • cari dalam array atau string - indexOf , misalnya: “123” .indexOf (2) = 1, [1, 2, 3] .indexOf (3) = 2. Mengembalikan -1 jika elemen tidak ditemukan.

Pembuatan fungsi saat ini tidak didukung.



Dokumentasi yang dikutip menyatakan bahwa "Kompatibilitas ECMAScript direncanakan." Tapi benarkah begitu? Mari kita coba mencari tahu bagaimana bahasa ini bekerja dari dalam.



Isi




  1. Mesin virtual VKScript
  2. Semantik objek VKScript
  3. Kesimpulan

Mesin virtual VKScript




Bagaimana suatu program dapat dianalisis dengan tidak adanya salinan lokal? Itu benar - kirim permintaan ke titik akhir publik dan analisis jawabannya. Mari kita coba, misalnya, untuk mengeksekusi kode berikut:



 while(1); 

Kami mendapatkan Runtime error occurred during code invocation: Too many operations . Ini menunjukkan bahwa dalam implementasi bahasa ada batasan pada jumlah tindakan yang dilakukan. Mari kita coba atur nilai batas yang tepat:


 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} - Kode berhasil dijalankan.

Dengan demikian, batas jumlah operasi adalah sekitar 1000 siklus "idle". Tetapi, pada saat yang sama, jelas bahwa siklus seperti itu kemungkinan besar bukan operasi "kesatuan". Mari kita coba menemukan operasi yang tidak dibagi oleh kompiler menjadi beberapa yang lebih kecil.


Kandidat yang paling jelas untuk peran operasi semacam itu adalah apa yang disebut pernyataan kosong ( ; ). Namun, setelah menambahkan kode dengan i < 999 50 karakter ; , batasnya tidak terlampaui. Ini berarti bahwa pernyataan kosong dilemparkan oleh kompiler dan tidak membuang operasi, atau satu iterasi dari loop membutuhkan lebih dari 50 operasi (yang, kemungkinan besar, tidak demikian).


Hal berikutnya yang terlintas dalam pikiran setelah ; - perhitungan beberapa ungkapan sederhana (misalnya, seperti ini: 1; ). Mari kita coba menambahkan beberapa ekspresi ini ke kode kita:


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

Jadi, 2 operasi 1; menghabiskan lebih banyak operasi dari 50 operasi ; . Ini mengkonfirmasi hipotesis bahwa pernyataan kosong tidak menyia-nyiakan instruksi.


Mari kita coba untuk mengurangi jumlah iterasi siklus dan tambahkan 1; tambahan 1; . Sangat mudah untuk memperhatikan bahwa untuk setiap iterasi ada 5 tambahan 1; oleh karena itu, satu iterasi siklus menghabiskan operasi 5 kali lebih banyak daripada satu operasi 1; .


Tetapi apakah ada operasi yang lebih sederhana? Sebagai contoh, menambahkan operator unary ~ tidak memerlukan perhitungan ekspresi tambahan, dan operasi itu sendiri dilakukan pada prosesor. Adalah logis untuk mengasumsikan bahwa menambahkan operasi ini ke ekspresi meningkatkan jumlah total operasi sebesar 1.


Tambahkan operator ini ke kode kami:


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

Dan ya, kita dapat menambahkan satu operator seperti itu, dan satu lagi ekspresi 1; - tidak lagi. Oleh karena itu, 1; benar-benar bukan operator kesatuan.


Mirip dengan operator 1; , kita akan mengurangi jumlah iterasi dari loop dan menambahkan ~ operator. Satu iterasi ternyata setara dengan 10 operasi kesatuan ~ , oleh karena itu, ekspresi 1; menghabiskan 2 operasi.


Perhatikan bahwa batasnya adalah sekitar 1000 iterasi, yaitu sekitar 10.000 operasi tunggal. Kami berasumsi bahwa batasnya persis 10.000 operasi.



Mengukur jumlah operasi dalam kode




Perhatikan bahwa sekarang kita dapat mengukur jumlah operasi dalam kode apa pun. Untuk melakukan ini, tambahkan kode ini setelah loop dan tambahkan / hapus iterasi, ~ operator, atau seluruh baris terakhir, sampai kesalahan Too many operations hilang.


Beberapa hasil pengukuran:


KodeJumlah operasi
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


Menentukan jenis mesin virtual




Pertama, Anda perlu memahami cara kerja juru bahasa VKScript. Ada dua opsi yang lebih atau kurang masuk akal:


  • Interpreter secara rekursif melintasi pohon sintaks dan melakukan operasi pada setiap node.
  • Kompiler menerjemahkan pohon sintaks ke dalam urutan instruksi yang dijalankan oleh penerjemah.

Sangat mudah untuk memahami bahwa VKScript menggunakan opsi kedua. Pertimbangkan ungkapannya (true?1:1); (5 operasi) dan (false?1:1); (4 operasi). Dalam hal eksekusi instruksi berurutan, operasi tambahan dijelaskan oleh transisi yang "mem-bypass" pilihan yang salah, dan dalam kasus pemintas AST rekursif, kedua opsi setara untuk penerjemah. Efek serupa juga terlihat pada if / else dengan kondisi berbeda.


Perlu juga memperhatikan pasangan i = 1; (3 operasi) dan var j = 1; (1 operasi). Membuat biaya variabel baru hanya 1 operasi, dan menetapkan untuk yang sudah ada biaya 3? Fakta bahwa membuat operasi biaya variabel 1 (dan, kemungkinan besar, ini adalah operasi pemuatan konstan), mengatakan dua hal:


  • Saat membuat variabel baru, tidak ada alokasi memori eksplisit untuk variabel.
  • Saat membuat variabel baru, nilai tidak dimuat ke dalam sel memori. Ini berarti bahwa ruang untuk variabel baru dialokasikan di mana nilai ekspresi dihitung, dan setelah itu memori ini dianggap dialokasikan. Ini menunjukkan penggunaan mesin stack.

Menggunakan tumpukan juga menjelaskan bahwa ekspresi var j = 1; berjalan lebih cepat dari ekspresi 1; : ekspresi terakhir menghabiskan instruksi tambahan untuk menghapus nilai yang dihitung dari tumpukan.



Menentukan nilai batas yang tepat


Perhatikan bahwa siklus var j=0;while(j < 1)j=j+1; (15 operasi) adalah salinan kecil dari siklus yang digunakan untuk pengukuran:


KodeJumlah operasi
 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; 

(batas)
9998

Hentikan apa? Apakah ada batasan 9998 instruksi? Kami jelas kehilangan sesuatu ...


Perhatikan bahwa kode return 1; adalah return 1; dilakukan sesuai dengan pengukuran untuk 0 instruksi. Ini mudah dijelaskan: kompiler menambahkan return null; implisit di akhir kode return null; , dan ketika menambahkan pengembaliannya gagal. Dengan asumsi bahwa batasnya adalah 10.000, kami menyimpulkan bahwa operasi return null; mengambil 2 instruksi (mungkin ini sesuatu seperti push null; return; ).



Blok Kode Bersarang




Mari kita lakukan beberapa pengukuran lagi:


KodeJumlah operasi
{};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

Mari kita perhatikan fakta-fakta berikut:


  • Menambahkan variabel ke blok membutuhkan satu operasi ekstra.
  • Ketika "mendeklarasikan variabel lagi" deklarasi kedua memenuhi sebagai tugas normal.
  • Tetapi pada saat yang sama, variabel di dalam blok tidak terlihat dari luar (lihat contoh terakhir).

Sangat mudah untuk memahami bahwa operasi tambahan dihabiskan untuk menghapus variabel lokal yang dinyatakan dalam blok dari stack. Dengan demikian, ketika tidak ada variabel lokal, tidak ada yang perlu dihapus.



Objek, Metode, Panggilan API




KodeJumlah operasi
"";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

Mari kita menganalisis hasilnya. Anda mungkin memperhatikan bahwa membuat string dan objek / array kosong membutuhkan 2 operasi, seperti halnya memuat angka. Saat membuat array atau objek yang tidak kosong, operasi yang dihabiskan untuk memuat elemen array / objek ditambahkan. Ini menunjukkan bahwa secara langsung membuat objek terjadi dalam satu operasi. Pada saat yang sama, tidak ada waktu yang terbuang untuk mengunduh nama properti, oleh karena itu, mengunduhnya adalah bagian dari operasi pembuatan objek.


Dengan pemanggilan metode API, semuanya juga sangat umum - memuat unit, memanggil metode, pop hasilnya (Anda dapat melihat bahwa nama metode diproses secara keseluruhan, dan bukan sebagai mengambil properti). Tetapi tiga contoh terakhir terlihat menarik.


  • "".substr(0, 0); - Memuat string, memuat nol, memuat nol, hasil pop . Karena suatu alasan, ada 2 instruksi untuk memanggil metode (untuk beberapa alasan, lihat di bawah).
  • var j={};jx=1; - membuat objek, memuat objek, memuat unit, unit pop setelah penugasan. Sekali lagi, ada 2 instruksi untuk penugasan.
  • var j={x:1};delete jx; - Memuat unit, membuat objek, memuat objek, menghapus. Ada 3 instruksi per operasi penghapusan.



Semantik objek VKScript


Angka-angka




Kembali ke pertanyaan awal: apakah VKScript adalah subset dari JavaScript atau bahasa lain? Mari kita lakukan tes sederhana:


 return 1000000000 + 2000000000; 

 {"response": -1294967296}; 

Seperti yang dapat kita lihat, penambahan bilangan bulat menyebabkan overflow, meskipun fakta bahwa JavaScript tidak memiliki bilangan bulat seperti itu. Juga mudah untuk memverifikasi bahwa membagi dengan 0 menyebabkan kesalahan, dan tidak mengembalikan Infinity .



Benda-benda




 return {}; 

 {"response": []} 

Hentikan apa? Kami mengembalikan objek dan mendapatkan array ? Ya, benar. Dalam VKScript, array dan objek diwakili oleh tipe yang sama, khususnya, objek kosong dan array kosong adalah satu dan sama. Dalam hal ini, properti length objek bekerja dan mengembalikan jumlah properti.


Sangat menarik untuk melihat bagaimana metode daftar berperilaku jika Anda memanggilnya pada suatu objek?


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

 3 

Metode pop mengembalikan properti yang terakhir dinyatakan, yang, bagaimanapun, adalah logis. Ubah urutan properti:


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

 3 

Rupanya, objek dalam VKScript mengingat urutan properti yang ditugaskan. Mari kita coba gunakan properti numerik:


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

 3 

Sekarang mari kita lihat bagaimana push bekerja:


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

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

Seperti yang Anda lihat, metode push mengurutkan tombol angka dan menambahkan nilai baru setelah tombol angka terakhir. "Lubang" tidak diisi dalam kasus ini.


Sekarang coba gabungkan dua metode ini:


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

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

Seperti yang kita lihat, elemen belum dihapus dari array. Namun, jika kita menempatkan push dan pop di baris yang berbeda, bug akan hilang. Kita harus masuk lebih dalam!



Penyimpanan Objek




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

 {"response": []} 

Ternyata, objek dalam VKScript disimpan dengan nilai, tidak seperti JavaScript. Sekarang kita melihat perilaku aneh dari string a.push(a.pop()); - rupanya, nilai lama array disimpan di stack, dari tempat kemudian diambil.


Namun, bagaimana data disimpan dalam objek jika metode memodifikasinya? Rupanya, instruksi "ekstra" saat memanggil metode ini dirancang khusus untuk menulis perubahan kembali ke objek.



Metode Array




MetodeAksi
push
  • urutkan kunci numerik berdasarkan nilai
  • ambil kunci angka maksimum, tambahkan satu
  • tulis argumen ke array
  • tambahkan kunci non-numerik ke akhir array
popHapus elemen terakhir dari array (tidak harus dengan kunci numerik) dan kembali.
sisanya
  • mengurutkan kunci numerik berdasarkan nilai, menghapus "lubang" dalam array
  • melakukan operasi javascript yang sesuai
  • tambahkan kunci non-numerik ke akhir array

Saat menggunakan metode slice, perubahan tidak disimpan



Kesimpulan




VKScript bukan JavaScript. Tidak seperti JavaScript, objek di dalamnya disimpan oleh nilai, bukan oleh referensi, dan memiliki semantik yang sama sekali berbeda. Namun, ketika menggunakan VKScript untuk tujuan yang dimaksudkan, perbedaannya tidak terlihat.



Semantik PS operator




Komentar yang disebutkan menggabungkan objek melalui + . Dalam hal ini, saya memutuskan untuk menambahkan informasi tentang pekerjaan operator.


OperatorTindakan
+
  • Jika kedua argumen adalah objek, buat salinan objek pertama dan tambahkan kunci dari argumen kedua (dengan penggantian).
  • Jika kedua argumen adalah angka, tambahkan sebagai angka.
  • Jika tidak, kedua operan dilemparkan ke string dan ditambahkan sebagai string.
Operator aritmatika lainnyaKedua operan dilemparkan ke nomor, dan operasi yang sesuai dilakukan. Untuk operasi bit, operan juga ditambahkan ke int .
Operator pembandingJika dua string atau dua angka dibandingkan, mereka dibandingkan secara langsung. Jika string dan angka dibandingkan, dan string adalah notasi yang benar untuk nomor tersebut, string tersebut dilemparkan ke angka. Jika tidak, Comparing values of different or unsupported types kesalahan dikembalikan.
Cast ke stringAngka dan string diberikan seperti dalam JavaScript. Objek terdaftar sebagai daftar nilai yang dipisahkan koma dalam urutan tombol. false dan null dilemparkan sebagai "" , true dilemparkan sebagai "1" .
Diputar keJika argumennya adalah string yang merupakan notasi angka yang valid, nomor tersebut dikembalikan. Kalau tidak, kesalahan Numeric arguments expected dikembalikan.

Untuk operasi dengan angka (kecuali untuk bit), jika operan int dan double , int double ke double . Jika kedua operan int , operasi dilakukan pada bilangan bulat 32-bit yang telah ditandatangani (dengan overflow).

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


All Articles