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:
Descrição do VKScript na página do método na documentação da API do VK (a única documentação oficial do idioma):
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
- Máquina virtual VKScript
- Semântica de objetos VKScript
- 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;
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:
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:
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:
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
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
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.
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).