VKScript语言分析:JavaScript,是吗?

TL; DR




VKScript 不是 JavaScript。 这种语言的语义从根本上不同于JavaScript的语义。 见结论


什么是VKScript?




VKScript是VKontakte execute API方法中使用的一种类似于JavaScript的脚本编程语言,它使客户能够准确下载所需的信息。 本质上,VKScript是Facebook用于相同目的的GraphQL的类似物。


比较GraphQL和VKScript:


GraphQLVK脚本
实作许多采用不同编程语言的开源实现VK API内唯一的实现
基于全新的语言Java脚本
可能性数据请求,有限过滤; 查询参数不能使用先前查询的结果客户自行决定对数据进行的任何后处理; API请求以方法的形式表示,可以使用先前请求中的任何数据

VK API文档 (唯一的官方语言文档)中的方法页面上的VKScript描述:


代号VKScript中的算法代码-一种类似于JavaScriptActionScript的格式(假定与ECMAScript兼容) 。 该算法应以命令return%expression%结束 。 运算符必须用分号分隔。

支持以下内容:


  • 算术运算
  • 逻辑运算
  • 创建数组和列表([X,Y])
  • parseIntparseDouble
  • 串联(+)
  • 如果构造
  • 按参数(@。)进行数组过滤
  • API方法调用, 长度参数
  • 使用while语句循环
  • Javascript方法: slicepushpopshiftunshiftsplicesubstrsplit
  • 删除运算符
  • 分配给数组元素,例如:row.user.action =“ test”;
  • 数组或字符串中的搜索为indexOf ,例如:“ 123” .indexOf(2)= 1,[1、2、3] .indexOf(3)=2。如果未找到该元素,则返回-1。

当前不支持函数创建。



引用的文档指出“已计划ECMAScript兼容性”。 但是是这样吗? 让我们尝试从内部了解这种语言的工作方式。



目录内容




  1. VKScript虚拟机
  2. VKScript对象的语义
  3. 结论

VKScript虚拟机




在没有本地副本的情况下如何分析程序? 没错-将请求发送到公共端点并分析答案。 例如,让我们尝试执行以下代码:



 while(1); 

我们收到Runtime error occurred during code invocation: Too many operationsRuntime error occurred during code invocation: Too many operations 。 这表明在执行该语言时,所执行的操作数是有限制的。 让我们尝试设置确切的极限值:


 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} -代码成功执行。

因此,操作次数的限制约为1000个“空闲”周期。 但是,与此同时,很明显,这种循环很可能不是“单一”的操作。 让我们尝试找到一个不会被编译器分成几个较小的操作的操作。


此类操作最明显的候选者是所谓的空语句( ; )。 但是,在将代码添加到i < 999 50个字符之后; ,不超过限制。 这意味着要么空语句由编译器抛出并且不浪费操作,要么循环的一次迭代需要50个以上的操作(很可能不是这样)。


之后想到的下一件事; -计算一些简单的表达式(例如: 1; )。 让我们尝试将其中一些表达式添加到我们的代码中:


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

因此,2个操作1; 花费的手术多于50次; 。 这证实了以下假设:空语句不会浪费指令。


让我们尝试减少循环的迭代次数,并增加1; 。 容易注意到,每次迭代有5个额外的1; 因此,循环的一次迭代所花的运算量比一个运算1;多5倍1;


但是,有没有更简单的操作? 例如,添加一元运算符~不需要计算其他表达式,并且运算本身在处理器上执行。 逻辑上假设将此操作添加到表达式中会使操作总数增加1。


将此运算符添加到我们的代码中:


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

是的,我们可以添加一个这样的运算符,再添加一个表达式1; -不再。 因此, 1; 确实不是单一运营商。


与运算符1;类似1; ,我们将减少循环的迭代次数并添加~运算符。 一次迭代等于10个unit运算~ ,因此,表达式1; 花费2次操作。


请注意,该限制约为1000次迭代,即大约10,000次单个操作。 我们假设该限制恰好是10,000次操作。



测量代码中的操作数




请注意,现在我们可以测量任何代码中的操作数。 为此,请在循环后添加此代码,并添加/删除迭代, ~运算符或最后一行,直到“ Too many operations错误消失。


一些测量结果:


代号操作次数
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


确定虚拟机的类型




首先,您需要了解VKScript解释器的工作方式。 有两种或多或少的合理选择:


  • 解释器递归地遍历语法树并在每个节点上执行操作。
  • 编译器将语法树转换为解释器执行的指令序列。

很容易理解,VKScript使用了第二个选项。 考虑一下表达式(true?1:1); (5个操作)和(false?1:1); (4次操作)。 在顺序执行指令的情况下,通过“绕过”错误的选项的转换来解释附加操作,在递归AST旁路的情况下,两个选项对于解释器都是等效的。 如果/否则,在不同条件下会观察到类似的效果。


另外,对i = 1;也是值得关注的i = 1; (3个运算),并且var j = 1; (1次操作)。 创建一个新变量仅花费1个操作,而分配给一个现有变量则花费3个操作? 创建变量需要花费1次操作(并且很可能是恒定的加载操作),这一事实说明了两点:


  • 创建新变量时,没有为该变量显式分配内存。
  • 创建新变量时,该值不会加载到存储单元中。 这意味着将在计算表达式值的位置分配用于新变量的空间,然后将其视为已分配的内存。 这建议使用堆栈机。

使用堆栈还可以解释表达式var j = 1; 比表达式1;运行更快1; :最后一个表达式花费额外的指令从堆栈中删除计算值。



确定确切的极限值


注意循环var j=0;while(j < 1)j=j+1; (15个操作)是用于测量的循环的一小部分副本:


代号操作次数
 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; 

(限制)
9998

停什么 是否有9998条指令的限制? 我们显然缺少了一些东西...


请注意, return 1;码为return 1; 根据0条指令的测量结果执行。 这很容易解释:编译器在代码的末尾添加了一个隐式的return null; ,并且在添加返回值时失败。 假设限制为10000,我们得出的结论是该操作return null; 需要2条指令(可能这类似于push null; return; )。



嵌套代码块




让我们进行更多测量:


代号操作次数
{};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

让我们注意以下事实:


  • 将变量添加到块中需要执行一项额外的操作。
  • 当“再次声明变量”时,第二个声明作为常规分配完成。
  • 但是同时,从外部看不到块内部的变量(请参见最后一个示例)。

很容易理解,花了额外的操作从堆栈中删除块中声明的局部变量。 因此,当没有局部变量时,不需要删除任何内容。



对象,方法,API调用




代号操作次数
"";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

让我们分析结果。 您可能会注意到,创建一个字符串和一个空数组/对象需要2次操作,加载数字也一样。 创建非空数组或对象时,将添加花费在加载数组/对象元素上的操作。 这表明在一个操作中直接创建对象。 同时,不浪费时间下载属性名称;因此,下载属性名称是创建对象的操作的一部分。


使用API​​方法调用,一切也很平常-加载一个单元,实际调用该方法, pop结果(您会注意到方法名称是作为一个整体处理的,而不是作为属性来处理的)。 但是最后三个示例看起来很有趣。


  • "".substr(0, 0); -加载字符串,加载零,加载零, pop结果。 由于某种原因,有2条关于调用方法的指令(出于某种原因,请参见下文)。
  • var j={};jx=1; -创建对象,加载对象,加载单元,分配后pop单元。 同样,有2条分配说明。
  • var j={x:1};delete jx; -加载单元,创建对象,加载对象,删除。 每个删除操作有3条指令。



VKScript对象的语义


数字




回到原始问题:VKScript是JavaScript或另一种语言的子集吗? 让我们做一个简单的测试:


 return 1000000000 + 2000000000; 

 {"response": -1294967296}; 

我们可以看到,尽管JavaScript没有这样的整数,但整数加法仍会导致溢出。 也很容易验证除以0会导致错误,并且不返回Infinity



对象




 return {}; 

 {"response": []} 

停什么 我们返回一个对象并得到一个数组 ? 是的,是的。 在VKScript中,数组和对象用相同的类型表示,特别是,一个空对象和一个空数组是相同的。 在这种情况下,对象的length属性起作用并返回属性的数量。


有趣的是,如果在对象上调用列表方法,它们的行为如何?


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

 3 

pop方法返回最后声明的属性,但这是合乎逻辑的。 更改属性的顺序:


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

 3 

显然,VKScript中的对象会记住分配属性的顺序。 让我们尝试使用数字属性:


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

 3 

现在,让我们看一下推送的工作原理:


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

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

如您所见,push方法对数字键进行排序,并在最后一个数字键之后添加一个新值。 在这种情况下不填充“孔”。


现在尝试结合这两种方法:


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

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

如我们所见,该元素尚未从数组中删除。 但是,如果我们将pushpop放在不同的行中,该错误将消失。 我们需要更深入!



对象存储




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

 {"response": []} 

事实证明,与JavaScript不同,VKScript中的对象是按值存储的。 现在我们看到了字符串a.push(a.pop());的奇怪行为a.push(a.pop()); -显然,数组的旧值保存在堆栈中,以后从那里获取。


但是,如果方法修改了该数据,该如何存储在对象中呢? 显然,调用该方法时的“额外”指令是专门为将更改写回到对象而设计的。



数组方法




方法动作片
push
  • 按值对数字键排序
  • 取最大数字键,加一个
  • 将参数写入数组
  • 将非数字键添加到数组的末尾
pop从数组中删除最后一个元素(不必使用数字键)并返回。
其余的
  • 按值对数字键进行排序,删除数组中的“孔”
  • 执行适当的JavaScript操作
  • 将非数字键添加到数组的末尾

使用切片方法时,不会保存更改



结论




VKScript不是JavaScript。 与JavaScript不同,JavaScript中的对象是按值存储的,而不是按引用存储的,它们具有完全不同的语义。 但是,将VKScript用于预期目的时,差异并不明显。



PS运算符语义




提到了通过+组合对象的注释。 在这方面,我决定添加有关操作员工作的信息。


操作员动作
+
  • 如果两个参数都是对象,请创建第一个对象的副本,然后将第二个对象的键(并替换)添加到该对象。
  • 如果两个参数都是数字,则添加为数字。
  • 否则,两个操作数都将转换为字符串并添加为字符串。
其他算术运算符两个操作数都强制转换为数字,并执行相应的操作。 对于位运算,操作数还强制转换为int
比较运算符如果比较两个字符串或两个数字,则直接比较它们。 如果将字符串和数字进行比较,并且该字符串是该数字的正确表示法,则该字符串将强制转换为数字。 否则,将返回“ Comparing values of different or unsupported types错误。
转换为字符串数字和字符串如JavaScript中所示。 对象按键顺序以逗号分隔的值列表形式列出。 falsenull强制转换为""true强制转换为"1"
投放到如果参数是一个有效数字符号的字符串,则返回数字。 否则, Numeric arguments expected返回Numeric arguments expected错误。

对于数字操作(位除外),如果操作数为intdouble ,则将int doubledouble 。 如果两个操作数均为int ,则对有符号的32位整数执行操作(带有溢出)。

Source: https://habr.com/ru/post/zh-CN464099/


All Articles