Análise da linguagem VKScript: JavaScript, e você?

TL; DR




VKScript não é JavaScript. A semântica dessa linguagem é fundamentalmente diferente da semântica do JavaScript. Veja a conclusão .


O que é o VKScript?




O VKScript é uma linguagem de programação de script semelhante a JavaScript usada no método de API de execute do VKontakte, que permite que os clientes baixem exatamente as informações necessárias. Em essência, o VKScript é um análogo do GraphQL usado pelo Facebook para o mesmo objetivo.


Compare GraphQL e VKScript:


GraphQLVKScript
ImplementaçõesMuitas implementações de código aberto em diferentes linguagens de programaçãoA única implementação na API VK
Baseado emNovo idiomaJavascript
As possibilidadesSolicitação de dados, filtragem limitada; argumentos de consulta não podem usar os resultados de consultas anterioresQualquer pós-processamento de dados, a critério do cliente; As solicitações de API são apresentadas na forma de métodos e podem usar qualquer dado de solicitações anteriores

Descrição do VKScript na página do método na documentação da API do VK (a única documentação oficial do idioma):


códigocódigo do algoritmo no VKScript - um formato semelhante ao JavaScript ou ActionScript (a compatibilidade com ECMAScript é assumida) . O algoritmo deve terminar com o comando return% expression% . Os operadores devem ser separados por ponto e vírgula.
corda

Os seguintes são suportados:


  • operações aritméticas
  • operações lógicas
  • criação de matrizes e listas ([X, Y])
  • parseInt e parseDouble
  • concatenação (+)
  • se construir
  • filtro de matriz por parâmetro (@.)
  • Chamadas do método API , parâmetro length
  • loops usando a instrução while
  • Métodos Javascript: fatia , push , pop , shift , unshift , emenda , substr , divisão
  • operador de exclusão
  • atribuição a elementos da matriz, por exemplo: row.user.action = "test";
  • a pesquisa na matriz ou cadeia de caracteres é indexOf , por exemplo: “123” .indexOf (2) = 1, [1, 2, 3] .indexOf (3) = 2. Retorna -1 se o elemento não for encontrado.

Atualmente, a criação de funções não é suportada.



A documentação citada afirma que "a compatibilidade do ECMAScript está planejada". Mas é assim? Vamos tentar descobrir como essa linguagem funciona por dentro.



Conteúdo




  1. Máquina virtual VKScript
  2. Semântica de objetos VKScript
  3. Conclusão

Máquina virtual VKScript




Como um programa pode ser analisado na ausência de uma cópia local? É isso mesmo - envie solicitações para o endpoint público e analise as respostas. Vamos tentar, por exemplo, executar o seguinte código:



 while(1); 

Runtime error occurred during code invocation: Too many operations um Runtime error occurred during code invocation: Too many operations . Isso sugere que na implementação da linguagem há um limite no número de ações executadas. Vamos tentar definir o valor limite exato:


 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} - O código executado com sucesso.

Assim, o limite no número de operações é de cerca de 1000 ciclos "inativos". Mas, ao mesmo tempo, fica claro que esse ciclo provavelmente não é uma operação "unitária". Vamos tentar encontrar uma operação que não seja dividida pelo compilador em várias menores.


O candidato mais óbvio para o papel de tal operação é a chamada declaração vazia ( ; ). No entanto, após adicionar ao código com i < 999 50 caracteres ; , o limite não é excedido. Isso significa que a instrução vazia é lançada pelo compilador e não desperdiça operações, ou uma iteração do loop leva mais de 50 operações (o que, provavelmente, não é assim).


A próxima coisa que vem à mente depois ; - cálculo de alguma expressão simples (por exemplo, assim: 1; ). Vamos tentar adicionar algumas dessas expressões ao nosso código:


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

Assim, 2 operações 1; gaste mais operações do que 50 operações ; . Isso confirma a hipótese de que a declaração vazia não desperdiça instruções.


Vamos tentar reduzir o número de iterações do ciclo e adicionar um 1; adicional 1; . É fácil perceber que, para cada iteração, existem 5 1; adicionais 1; portanto, uma iteração do ciclo passa 5 vezes mais operações que uma operação 1; .


Mas existe uma operação ainda mais simples? Por exemplo, adicionar o operador unário ~ não requer o cálculo de expressões adicionais e a própria operação é executada no processador. É lógico supor que adicionar essa operação à expressão aumente o número total de operações em 1.


Adicione este operador ao nosso código:


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

E sim, podemos adicionar um desses operadores e mais uma expressão 1; - não mais. Portanto, 1; realmente não é um operador unitário.


Semelhante ao operador 1; , reduziremos o número de iterações do loop e adicionaremos os operadores ~ . Uma iteração acabou sendo equivalente a 10 operações unitárias ~ , portanto, a expressão 1; passa 2 operações.


Observe que o limite é de aproximadamente 1000 iterações, ou seja, aproximadamente 10.000 operações únicas. Assumimos que o limite é exatamente 10.000 operações.



Medindo o número de operações no código




Observe que agora podemos medir o número de operações em qualquer código. Para fazer isso, adicione esse código após o loop e adicione / remova iterações, ~ operadores ou toda a última linha, até que o erro Too many operations desapareça.


Alguns resultados de medição:


CódigoNúmero de operações
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


Determinando o tipo de máquina virtual




Primeiro, você precisa entender como o intérprete VKScript funciona. Existem duas opções mais ou menos plausíveis:


  • O intérprete percorre recursivamente a árvore de sintaxe e executa uma operação em cada nó.
  • O compilador converte a árvore de sintaxe em uma sequência de instruções que o intérprete executa.

É fácil entender que o VKScript usa a segunda opção. Considere a expressão (true?1:1); (5 operações) e (false?1:1); (4 operações). No caso de execução seqüencial de instruções, uma operação adicional é explicada por uma transição que "ignora" a opção errada e, no caso de um desvio AST recursivo, ambas as opções são equivalentes para o intérprete. Um efeito semelhante é observado em if / else com uma condição diferente.


Também vale a pena prestar atenção no par i = 1; (3 operações) e var j = 1; (1 operação). Criar uma nova variável custa apenas 1 operação e atribuir a uma existente custa 3? O fato de criar uma variável custa 1 operação (e, provavelmente, é uma operação de carregamento constante), diz duas coisas:


  • Ao criar uma nova variável, não há alocação de memória explícita para a variável.
  • Ao criar uma nova variável, o valor não é carregado na célula de memória. Isso significa que o espaço para a nova variável é alocado onde o valor da expressão foi calculado e, depois disso, essa memória é considerada alocada. Isso sugere o uso de uma máquina de empilhar.

O uso da pilha também explica que a expressão var j = 1; corre mais rápido que a expressão 1; : a última expressão passa instruções adicionais sobre como remover o valor calculado da pilha.



Determinando o valor limite exato


Observe que o ciclo var j=0;while(j < 1)j=j+1; (15 operações) é uma cópia pequena do ciclo que foi usado para medições:


CódigoNúmero de operações
 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

Parar o que? Existe um limite de 9998 instruções? Estamos claramente perdendo algo ...


Observe que o código de return 1; é return 1; realizada de acordo com as medições para 0 instruções. Isso é facilmente explicado: o compilador adiciona um return null; implícito return null; no final do código return null; e, ao adicionar seu retorno, ele falha. Supondo que o limite seja 10000, concluímos que a operação return null; leva 2 instruções (provavelmente isso é algo como push null; return; ).



Blocos de código aninhados




Vamos fazer mais algumas medições:


CódigoNúmero de operações
{};0 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

Vamos prestar atenção aos seguintes fatos:


  • Adicionar uma variável a um bloco requer uma operação extra.
  • Ao "declarar uma variável novamente", a segunda declaração é cumprida como uma atribuição normal.
  • Mas, ao mesmo tempo, a variável dentro do bloco não é visível do lado de fora (veja o último exemplo).

É fácil entender que uma operação extra é gasta na remoção de variáveis ​​locais declaradas no bloco da pilha. Portanto, quando não há variáveis ​​locais, nada precisa ser excluído.



Objetos, métodos, chamadas de API




CódigoNúmero de operações
"";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

Vamos analisar os resultados. Você pode perceber que a criação de uma sequência de caracteres e de uma matriz / objeto vazio leva duas operações, assim como o carregamento de um número. Ao criar uma matriz ou objeto não vazio, são adicionadas as operações gastas no carregamento de elementos da matriz / objeto. Isso sugere que a criação direta de um objeto ocorre em uma operação. Ao mesmo tempo, não se perde tempo baixando nomes de propriedades; portanto, fazer o download deles faz parte da operação de criação do objeto.


Com a chamada do método API, tudo também é bastante comum - carregar uma unidade, na verdade chamando o método, pop resultado (você pode perceber que o nome do método é processado como um todo, e não como propriedades). Mas os últimos três exemplos parecem interessantes.


  • "".substr(0, 0); - carregando uma string, carregando zero, carregando zero, resultado pop . Por um motivo, existem 2 instruções para chamar um método (por algum motivo, veja abaixo).
  • var j={};jx=1; - criando um objeto, carregando um objeto, carregando uma unidade, pop unidade após a atribuição. Novamente, existem 2 instruções para atribuição.
  • var j={x:1};delete jx; - carregando uma unidade, criando um objeto, carregando um objeto, excluindo. Existem 3 instruções por operação de exclusão.



Semântica de objetos VKScript


Os números




Voltando à pergunta original: o VKScript é um subconjunto de JavaScript ou outro idioma? Vamos fazer um teste simples:


 return 1000000000 + 2000000000; 

 {"response": -1294967296}; 

Como podemos ver, a adição de números inteiros leva ao estouro, apesar do JavaScript não possuir números inteiros. Também é fácil verificar se a divisão por 0 gera um erro e não retorna o Infinity .



Os objetos




 return {}; 

 {"response": []} 

Parar o que? Retornamos um objeto e obtemos uma matriz ? É sim. No VKScript, matrizes e objetos são representados pelo mesmo tipo, em particular, um objeto vazio e uma matriz vazia são a mesma coisa. Nesse caso, a propriedade length do objeto funciona e retorna o número de propriedades.


É interessante ver como os métodos de lista se comportam se você os chama em um objeto?


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

 3 

O método pop retorna a última propriedade declarada, que, no entanto, é lógica. Altere a ordem das propriedades:


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

 3 

Aparentemente, os objetos no VKScript lembram a ordem em que as propriedades são atribuídas. Vamos tentar usar propriedades numéricas:


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

 3 

Agora vamos ver como o push funciona:


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

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

Como você pode ver, o método push classifica as teclas numéricas e adiciona um novo valor após a última tecla numérica. "Buracos" não são preenchidos neste caso.


Agora tente combinar estes dois métodos:


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

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

Como vemos, o elemento não foi excluído da matriz. No entanto, se colocarmos push e pop em linhas diferentes, o bug desaparecerá. Precisamos ir mais fundo!



Armazenamento de Objetos




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

 {"response": []} 

Como se viu, os objetos no VKScript são armazenados por valor, diferentemente do JavaScript. Agora vemos o estranho comportamento da string a.push(a.pop()); - aparentemente, o valor antigo da matriz foi salvo na pilha, de onde foi retirado posteriormente.


No entanto, como então os dados são armazenados no objeto se o método o modifica? Aparentemente, a instrução "extra" ao chamar o método foi projetada especificamente para gravar alterações no objeto.



Métodos de matriz




MétodoAcção
push
  • classificar chaves numéricas por valor
  • pegue a tecla numérica máxima, adicione uma
  • escrever argumento na matriz
  • adicione chaves não numéricas ao final da matriz
popRemova o último elemento da matriz (não necessariamente com uma tecla numérica) e retorne.
o resto
  • classifique as teclas numéricas por valor, remova os “buracos” na matriz
  • executar operação javascript apropriada
  • adicione chaves não numéricas ao final da matriz

Ao usar o método de fatia, as alterações não são salvas



Conclusão




VKScript não é JavaScript. Ao contrário do JavaScript, os objetos nele são armazenados por valor, não por referência e possuem semânticas completamente diferentes. No entanto, ao usar o VKScript para a finalidade a que se destina, a diferença não é perceptível.



PS Semântica de operadores




Os comentários mencionados combinando objetos via + . A esse respeito, decidi acrescentar informações sobre o trabalho dos operadores.


OperadorAcções
+
  • Se os dois argumentos forem objetos, crie uma cópia do primeiro objeto e adicione as chaves do segundo (com substituição) a ele.
  • Se os dois argumentos forem números, adicione como números.
  • Caso contrário, os dois operandos são convertidos em uma sequência e adicionados como sequências.
Outros operadores aritméticosAmbos os operandos são convertidos em um número e a operação correspondente é executada. Para operações de bit, os operandos também são convertidos para int .
Operadores de comparaçãoSe duas cadeias ou dois números são comparados, eles são comparados diretamente. Se uma sequência e um número forem comparados, e a sequência for uma notação correta para o número, a sequência será convertida em um número. Caso contrário, um erro Comparing values of different or unsupported types será retornado.
Transmitir para sequênciaNúmeros e seqüências de caracteres são fornecidos como no JavaScript. Os objetos são listados como uma lista de valores separados por vírgula na ordem das chaves. false e null são convertidos como "" , true convertido como "1" .
Transmitir paraSe o argumento for uma sequência que é uma notação de número válida, o número será retornado. Caso contrário, um erro Numeric arguments expected será retornado.

Para operações com números (exceto bit), se os operandos são int e double , int é double em double . Se os dois operandos forem int , uma operação será executada em números inteiros de 32 bits assinados (com estouro).

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


All Articles