QuickJs 剖析

 

Value 表示

在动态语言的VM中,由于通常变量的值都不是静态类型的,都是可变的,另一方面动态类型的值一般涉及到Gc的设计实现,因此不能直接使用特定类型来直接表示值,而一般抽象出一层Value来统一表示所有的值,即这个Value有可能为null,有可能为undefined,有可能为number,那么就需要将不同值来编码到另一个通用表示,简单来说是一个映射关系。通常的编码方式包括Tagged Value和Nan Boxing,Tagged Value跟union类似,我们在不知道值的情况下,可以加个tag解决,读取值时,先读取tag,再根据tag拿到对应的有意义的值,这种方法很简单,但是需要多一个字长来存储tag,通常64位对齐的情况下,就需要64位来存储tag,这样很浪费内存,当然也有类似Tagged Pointer的hack方法,利用指针LSB来存储一些信息(如v8中small integer的表示)。Nan boxing则是另一种编码方式,不依赖显示的tag,而是将所有信息都编码到一个64位的浮点数中,一般使用quiet nan剩下的可用编码位来存储信息,比如一个浮点数,64位,1位用来存储sign,11位用来存储exponent,52位用来存储magnitude,Quiet Nan的表示为11位全为1,52位的首位是1,那么还剩52位可以用来编码信息,比如可以一部分位用来存放tag,另一部分位用来存放除了浮点数的value的值,由于一般64位机器上指针只会用到48位,所以存储指针也是够用的,当然在jscore中还有shift的优化,即给所有的编码统一加一个偏移量,使得ptr的剩余16位全为0,这样可以大大加快指针读取的速度。

qjs中会使用不同的编码方式,因此操纵value时一定要使用定义好的宏,这里以Tagged Value为例

typedef union JSValueUnion {
    int32_t int32;
    double float64;
    void *ptr;
} JSValueUnion;

typedef struct JSValue {
    JSValueUnion u;
    int64_t tag;
} JSValue;

enum {
    JS_TAG_SYMBOL      = -8,
    JS_TAG_STRING      = -7,
    JS_TAG_MODULE      = -3, /* used internally */
    JS_TAG_FUNCTION_BYTECODE = -2, /* used internally */
    JS_TAG_OBJECT      = -1,

    JS_TAG_INT         = 0,
    JS_TAG_BOOL        = 1,
    JS_TAG_NULL        = 2,
    JS_TAG_UNDEFINED   = 3,
    JS_TAG_UNINITIALIZED = 4,
    JS_TAG_CATCH_OFFSET = 5,
    JS_TAG_EXCEPTION   = 6,
    JS_TAG_FLOAT64     = 7,
};

tag的设计用于区分不同类型的值即可,比如这里也可以为Bool增加True和False的tag,这样不用读取实际值,会更快,这里quickjs中关于tag还有一个小细节,即所有带Ref的tag都是负数,这样可以快速判断是否需要dup等。Primitive类型的值,如int,bool,float等,直接读取union中的值即可,对于string,symbol来讲,则会使用ptr指向JSString,JSString中存储对应的字符串或Symbol的值,剩下最重要的就是JsObject了,JsObject即类似Js中Object的引用类型,JsObject中需要存储prototype来支持Js基于原型链的继承方式,同时需要支持属性的增,删,改,查(当然也需要支持prototype的链式查找),同时JsObject作为Function,Array,Number,String,Regexp等类型的基础载体,需要支持存放额外数据,如Number的数值,Regexp的正则表达式等。JSObject的大致结构如下,prop用于存储所有的属性,shape中会存储对应的prototype,u中会存放实际额外的数据。

typedef struct JSObject {
    /* byte offsets: 16/24 */
    JSShape *shape; /* prototype and property names + flag */
    JSProperty *prop; /* array of properties */
    /* byte offsets: 28/48 */
    union {
        struct { /* JS_CLASS_C_FUNCTION: 12/20 bytes */
            JSContext *realm;
            JSCFunctionType c_function;
            uint8_t length;
            uint8_t cproto;
            int16_t magic;
        } cfunc;
        JSRegExp regexp;    /* JS_CLASS_REGEXP: 8/16 bytes */
        JSValue object_data;    /* for JS_SetObjectData(): 8/16/16 bytes */
    } u;

}

JSShape的结构如下,其主要用于存储JSObject中的属性名对应的prop中的索引

struct JSShape {
    uint32_t prop_hash_mask;
    int prop_size; /* allocated properties */
    int prop_count; /* include deleted properties */
    int deleted_prop_count;
    JSObject *proto;
    JSShapeProperty prop[0]; /* prop_size elements */
};

typedef struct JSShapeProperty {
    uint32_t hash_next : 26; /* 0 if last in list */
    uint32_t flags : 6;   /* JS_PROP_XXX */
    JSAtom atom; /* JS_ATOM_NULL = free property entry */
} JSShapeProperty;

JSShape中使用动态哈希表实现prop的存储,JSShapeProperty中存放属性名及对应的flag(configurable,writable,enumerable等),查属性的大致过程如下

h = (uintptr_t)atom & sh->prop_hash_mask;
while (h) {
    pr = &prop[h - 1];
    if (likely(pr->atom == atom)) {
        *ppr = &p->prop[h - 1];
        /* the compiler should be able to assume that pr != NULL here */
        return pr;
    }
    h = pr->hash_next;
}

由于会存在hash冲突,每次检查name是否一致,若不一致,则查找下一个对应相同hash的属性,查找到后,即返回JSObject中prop数组的索引,拿到索引后即可拿到实际属性的值。增加属性时,即先在JSShape中增加属性,再到JSObject中增加对应的prop即可,设置属性时,按照原型链的顺序,依次在JSShape中查找对应的属性名,若找到后,直接修改对应JSObject中的value即可,删除属性时,直接在JSShape的动态哈希表中删除,再删除JSObject中的值即可。这里的JSShape类似于v8中的hidden map,都是用于优化当存在大量对象的属性名相同时对象的属性查找问题,比如如果有大量对象的属性名完全一致,这样就可以多个对象共用一个JSShape,同时也可以使用Inline Cache进行优化,当检查到对象的Shape一致时,就可以使用已经缓存的属性索引直接查找到JSObject中的值。当然,JSObject是易变的,由于Object可以任意增删属性,这会导致原本一样Shape的对象不能共用一个Shape。quickjs中采用hash的方式来管理JSShape,简单来说,hash的JSShape是可以共用的,不hash的JSShape是不可以共用的,当一个对象首次创建时,会先在可hash的JSShape池中查找是否有跟当前对象的prototype一样且属性个数为0的JSShape,如果有则会直接复用该JSShape,若没有则新建一个,当给对象增加属性时,会先检查当前JSShape的hash值,如果是不hash的,则直接修改即可,如果是hash的,且当前只有这一个对象在使用这个JSShape,那么也可以直接改当前JSShape,如果有多个,那么必须先clone该JSShape之后,再进行修改,这里quickjs对增加属性有一个简单的transition规则,即首先会查找当前hash的JSShape池里是否有刚好比当前JSShape多一个要增加属性的JSShape,如果有,则直接使用该JSShape。当删除属性时,跟增加属性类似,quickjs中会对被删除的属性先留空,当发现删除过多属性时,会对prop进行压缩,回收使用的空间。quickjs当前使用JSShape时不会对constructor进行优化,以下面为例

function Point(){
    this.x = 1;
    this.y = 2;
}

第一次创建时,quickjs会创建一个空的JSShape,接着由于只有该对象会使用这个JSShape,后面添加x,y时会直接在该JSShape上进行修改,这就导致下次再次调用Point函数时,无法找到空的JSShape,而必须创建一个,当添加x属性时也是,只有添加y属性时可以复用JSShape。JSObject同时还需承载其他类型的值数据,比如Function,Regexp,quickjs中使用class_id来存储当前Object实际的数据类型,这样可以根据class_id来获取对应的value,以Function(Native)为例,

JSValue func_obj;
JSObject *p;
func_obj = JS_NewObjectProtoClass(ctx, proto_val, JS_CLASS_C_FUNCTION);
p = JS_VALUE_GET_OBJ(func_obj);
p->u.cfunc.c_function.generic = func;

这里在u中直接存放func的指针,再以Number为例,在u的object_data中存放实际Primitive的值,Stirng,Symbol等与此类似

if (JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT) {
    p = JS_VALUE_GET_OBJ(obj);
    switch(p->class_id) {
    case JS_CLASS_NUMBER:
    case JS_CLASS_STRING:
    case JS_CLASS_BOOLEAN:
    case JS_CLASS_SYMBOL:
    case JS_CLASS_DATE:
        JS_FreeValue(ctx, p->u.object_data);
        p->u.object_data = val;
        return 0;
    }
}

Parse流程

一般对于VM来讲,源代码语言需经过lexer,parser,compiler,interpreter几个阶段,最终执行。lexer解析源代码获取token流,parser获取token流解析为AST,compiler获取AST编译为字节码,interpreter获取字节码执行,一般来讲,lexer和parser是编译流水线中较简单的部分。当然这几个阶段并不一定是严格串行的,也不是严格独立的,比如quickjs中lexer,parser,compiler是在一遍解析中完成的,即在单遍pass中就完成了由字符流到字节码流。

Lexer实现

lexer的主要任务是读取字符流,转为token流,token流则是后续解析中可识别的最小单元,通常token包括各种运算符,变量名,关键字等。在解析时,依次读取每个字符,根据字符判断返回token即可,quickjs中lexer主要实现在next_token函数。当然lexer中也有许多优化,如 https://v8.dev/blog/scanner。以quickjs中token定义为例,主要分为四种,包括TOK_NUMBER,TOK_STRING这种带value的token,比如number和string的literal,需要在token中存储literal的值,以及TOK_LT等运算符,以及TOK_CLASS等关键词,当然最后还包括EOF,表示字符流结束。

Atom实现

由于在parse过程中会出现大量重复的字符串,比如变量名会重复使用,各种关键词会重复出现,如果在token中直接存储对应的字符串值,比如直接在TOK_IDENT中存储变量名,这样会造成大量的空间浪费,在比较时也很费时间,因此一般会采用类似符号表的做法,将相同字符串存储在一起,用一个唯一ID表示即可,quickjs中即用atom来代表这种表示某个字符串的唯一ID。此外,Atom还会使用MSB区分是否为number。

static LEPUSAtom LEPUS_NewAtomStr(LEPUSContext *ctx, LEPUSString *p) {
  LEPUSRuntime *rt = ctx->rt;
  uint32_t n;
  if (is_num_string(&n, p)) {
    if (n <= LEPUS_ATOM_MAX_INT) {
      lepus_free_string(rt, p);
      return __JS_AtomFromUInt32(n);
    }
  }
  return __JS_NewAtom(rt, p, LEPUS_ATOM_TYPE_STRING);
}

Parse算法

一般对于Parse主要有LL和LR两类解析算法,主要是通过CFG来进行归约或推导。quickjs中主要采用LL加pratt parser的方法进行解析。https://en.wikipedia.org/wiki/Operator-precedence_parser

以Eval为例,在解析时是作为一个function的,由js_parse_program进入后,首先js_parse_directives,主要用来解析’use strict’这种directive指令,接着parse_source_element,即依次解析合法的语句,在parse_source_element中,parse_function_decl负责解析function定义,parse_export负责解析export语句,parse_import负责解析import语句,parse_statement_or_decl则用于解析各种statement和declaration。

static __exception int lepus_parse_source_element(LEPUSParseState *s) {
  if (s->token.val == TOK_FUNCTION ||
      (token_is_pseudo_keyword(s, LEPUS_ATOM_async) &&
       peek_token(s, TRUE) == TOK_FUNCTION)) {
    if (lepus_parse_function_decl(s, LEPUS_PARSE_FUNC_STATEMENT,
                                  LEPUS_FUNC_NORMAL, LEPUS_ATOM_NULL,
                                  s->token.ptr, s->token.line_num))
      return -1;
  } else if (s->token.val == TOK_EXPORT && fd->module) {
    if (lepus_parse_export(s)) return -1;
  } else if (s->token.val == TOK_IMPORT && fd->module &&
             peek_token(s, FALSE) != '(') {
    if (lepus_parse_import(s)) return -1;
  } else {
    if (lepus_parse_statement_or_decl(s, DECL_MASK_ALL)) return -1;
  }
  return 0;
}

在parse_function_decl中,先parse_argument后,之后依旧parse_source_element,对于statement类的function declaration,由于不受重定义的影响,quickjs未对其进行redeclaration的检查,当然这也是qjs现有的一个bug,比如下面,qjs会编译通过,实际上应该为sayHello Syntax Error

let sayHello = 1;

function sayHello(){}

在parse_statement_or_decl中,根据当前token,进行对应的语句解析即可,以throw为例

case TOK_THROW:
    if (next_token(s))
        goto fail;
    if (js_parse_expr(s))
        goto fail;
    break;

在变量声明中,主要有三种情况,const,let和var(当然还包括catch)。变量声明统一在parse_var中处理,声明解析主要分为三步,一是判断变量类型,以进行对应的scope检查,二是获取变量名并预定义变量,三是解析初始值表达式。Function中存储变量结构如下

typedef struct JSFunctionDef {
    JSVarDef *vars;
    int scope_first; /* index into vd->vars of first lexically scoped variable */
    int scope_level; /* index into fd->scopes if the current lexical scope */
    JSVarScope *scopes;
}

typedef struct JSVarScope {
    int parent;  /* index into fd->scopes of the enclosing scope */
    int first;   /* index into fd->vars of the last variable in this scope */
} JSVarScope;

scopes是包含所有scope的数组,简单来说,每个scope中有对应parent scope的index,以及最后一个该scope中的变量index,而function中存储当前scope的索引及当前scope对应的第一个(其实是最后一个lexically scoped的变量),lexically scoped指得就是let和const声明的变量,对于var声明的变量,由于声明提升会做hoist处理。对于let和const声明的变量,首先会使用find_lexical_decl查找在当前scope是否存在相同命名的lexically的变量,大致查找过程如下

while (scope_idx >= 0) {
    JSVarDef *vd = &fd->vars[scope_idx];
    if (vd->var_name == name && vd->is_lexical))
        return scope_idx;
    scope_idx = vd->scope_next;
}

由于lexically的查找有GLOBAL_VAR_OFFSET限制,理论上单个函数的local variable数量是不能超过0x40000000个的,如果没有再接着看是否和function的argument有冲突

for(i = fd->arg_count; i-- > 0;) {
    if (fd->args[i].var_name == name)
        return i | ARGUMENT_VAR_OFFSET;
}

如果没有,再接着看是否跟hoist的var声明的变量有冲突,scope_level为0即代表是var声明的变量,is_child_scope即判断var声明的是否在当前scope的子scope中

for(i = 0; i < fd->var_count; i++) {
    JSVarDef *vd = &fd->vars[i];
    if (vd->var_name == name && vd->scope_level == 0) {
        if (is_child_scope(ctx, fd, vd->scope_next,
                           scope_level))
            return i;
    }
}

这些检查都没有问题后,即可加入到当前function的变量列表中,idx即为vars的索引,同时会更新对应scope的最新的lexically的变量索引

int idx = add_var(ctx, fd, name);
if (idx >= 0) {
    JSVarDef *vd = &fd->vars[idx];
    vd->var_kind = var_kind;
    vd->scope_level = fd->scope_level;
    vd->scope_next = fd->scope_first;
    fd->scopes[fd->scope_level].first = idx;
    fd->scope_first = idx;
}

而对于var声明的变量,首先也会find_lexical_decl检查是否有重名的lexically的variable,接着会find_var检查是否有重名的var声明的以及函数参数的变量,这里如果有则会直接返回,并不会抛出Syntax error,如果没有则直接add_var加入到变量列表中。除了上述情况下,还有一种特殊情况需要处理,那就是全局变量,全局是作为一个function进行编译的,在global中定义的变量也就是全局变量,对于全局变量同样会进行上述的scope检查,不过会用JsGlobalVar统一管理,在执行enter_scope时统一初始化。此外,对于global的变量,qjs在lookup时会使用名字进行查询,而对于function内部的local variable,则会使用直接使用opcode后面的index直接索引对应的变量,所以一般情况下,多在function内使用局部变量也比全局变量要快,同时qjs内部还对前几个局部变量做了优化,直接使用特定的opcode来索引对应的局部变量,目前支持前4个局部变量的直接索引,即不用读取对应的index。 除了declaration之外,剩下的就是statement。statement中比较重要的是branch以及expr的解析,branch一般包括for循环,while循环,if判断,这些都可以使用LL直接解析,而对于expr,由于有很多运算符,且有优先级限制,当然也可以使用LL强制优先级进行解析,但这样会比较麻烦,一般可采用operator precedence的解析方法,大致思路如下

SN Parser::ParseBinaryExpression(SN left, int precedence)
{
  while (1)
  {
    if (CheckIsBianryOp(lexer_->current_token()))
    {
      auto op = GetBinaryOpFromToken(lexer_->current_token());
      auto next_precedence = GetBinaryOpPrecedence(op);
      if (next_precedence <= precedence)
      {
        return left;
      }
      else
      {
        lexer_->GetToken();
        auto next_left = ParseUnaryExpression();
        auto next_right =
            ParseBinaryExpression(move(next_left), next_precedence);
        left =
            make_shared<BinaryExpressionNode>(op, move(left), move(next_right));
      }
    }
    else
    {
      return left;
    }
  }
}

在每次进入解析binary表达式前,都会带一个当前operator的优先级,解析时先判断是否为一个合法的binary op,如果是则拿到operator对应的优先级后,判断这个优先级是否大于解析进来时传递的优先级,如果不大于,则说明当前operator的优先级并不比前一个高,这个时候就可以按照从左至右的顺序进行解析,直接返回即可,如果优先级更高,则可以与前一个先结合,这样就保证优先级的规则,比如1+1+1*2,在第一次解析到第二个+时,会直接返回,那么1+1就会结合在一起,接着第二次解析到*时,由于优先级更高,则1*2会结合在一起,最后(1+1)和(1*2)再结合到一起。qjs中解析表达式依次是parse_expr,parse_assign_expr,parse_cond_expr,parse_coalesce_expr,parse_logical_and_or,parse_expr_binary,在bianry_expr前是按照优先级顺序组织的,在binary_expr中则使用到level来判断优先级,level越小,优先级越高

static __exception int js_parse_expr_binary(JSParseState *s, int level,
                                            int parse_flags)
{
    int op, opcode;

    if (level == 0) {
        return js_parse_unary(s, (parse_flags & PF_ARROW_FUNC) |
                              PF_POW_ALLOWED);
    }
    if (js_parse_expr_binary(s, level - 1, parse_flags))
        return -1;
    for(;;) {
        op = s->token.val;
        switch(level) {
        case 1:
            switch(op) {
            case '*':
                opcode = OP_mul;
                break;
            }
            break;
        case 2:
            switch(op) {
            case '+':
                opcode = OP_add;
                break;
            }
            break;
        }
        if (next_token(s))
            return -1;
        if (js_parse_expr_binary(s, level - 1, parse_flags & ~PF_ARROW_FUNC))
            return -1;
        emit_op(s, opcode);
     }
}

当然,这里也有可以优化的地方,由于在解析表达式时会有大量的递归调用,这会造成不小的开销,可以尝试改写为循环。

Reference

  • https://en.wikipedia.org/wiki/Operator-precedence_parser
  • https://v8.dev/blog/preparser
  • https://v8.dev/blog/scanner
  • https://github.com/jameslahm/yajp

Bytecode Compiler

这里的Compiler主要是指Bytecode Compiler,即在quickjs单遍parse中直接生成的bytecode。bytecode作为一种IR,起着承上启下的作用,承上作为比AST更low level的中间表示,可以交给interpreter执行,由于其良好的local性和小空间,效率比AST执行更快,启下作为native code的输入流,可以在bytecod级别做优化,生成更好的机器码。bytecode主要分为两类,对应两类不同的VM,分别是Stack based和Register based,stack based的特点是所有的操作数都是基于栈的,比如add的opcode,在执行时会默认从栈中弹出两个数,加完之后在push回去。register based的特点则是基于寄存器的,比如在add的opcode中,后面会指定两个寄存器用于做运算。Stack based的bytecode实现起来较为容易,但有频繁访问栈的缺点,比如有给栈加一个acc的寄存器的优化,这样每次可少读一次栈,而register based实现起来比stack based的更为困难,还需涉及到寄存器分配等操作。quickjs中采用的则是基于stack的bytecode。

字节码设计

Quickjs中所有的字节码都在quickjs-opcode.h这个文件中,opcode主要分为三类,一类是基础使用的字节码,这部分字节码在编译及最终执行中都会用到,另一类是临时opcode,这类会在编译完后解析或者优化的过程中被替换掉或删掉,最后一类是short opcode的优化,quickjs中是默认开启的,针对部分opcode进行相应的优化,比如push_i32是push一个32位的值,当这个常量在8位内时,既可以直接使用push_i8代替,这样原本字节码后面需要32位现在只需要8位代替,大大缩小字节码占用空间。

字节码语义

字节码需要能够完全替代AST,在interpreter下能表示所有program的semantic,主要的语义有以下几类

  • branch,包括if,for,while等需要分支判断,跳转的语句
  • operator,运算符,主要用于各种表达式计算
  • push,push进栈各种值,包括常量值,属性名值,JSValue等
  • call,主要用于函数调用
  • throw,主要用于抛出异常及错误处理
  • var,主要用于获取,修改局部变量,全局变量等
  • field,主要是用于获取,修改对象属性 下面简单介绍一下各个opcode的语义
  • OP_push_i32 向栈中push一个32位的值
  • OP_push_const 向栈中push一个常量池中的值
  • OP_push_minus1 向栈中push一个-1
  • OP_push_0 向栈中push一个0
  • OP_push_1 向栈中push一个1
  • OP_push_2 向栈中push一个2
  • OP_push_3 向栈中push一个3
  • OP_push_4 向栈中push一个4
  • OP_push_5 向栈中push一个5
  • OP_push_6 向栈中push一个6
  • OP_push_7 向栈中push一个7
  • OP_push_i8 向栈中push一个8位整数
  • OP_push_i16 向栈中push一个16位整数
  • OP_push_const8 向栈中push一个常量,该常量索引为8位整数
  • OP_fclosure8 向栈中push一个闭包对象,该常量索引位8位整数
  • OP_push_empty_string 向栈中push一个空串
  • OP_get_length 获取对象的length属性
  • OP_push_atom_value 向栈中push atom代表的JSValue
  • OP_undefined 向栈中push一个undefined值
  • OP_null 向栈中push一个null值
  • OP_push_this 向栈中push this当前的值
  • OP_push_false 向栈中push一个false
  • OP_push_true 向栈中push一个true
  • OP_object 向栈中push一个新创建的object
  • OP_special_object
  • OP_rest
  • OP_drop 删除栈顶上的值
  • OP_nip 删除栈顶第二个值
  • OP_nip1 删除栈顶第三个值
  • OP_dup 复制栈顶的值
  • OP_dup2 复制栈顶的前两个值
  • OP_dup3 复制栈顶的前三个值
  • OP_dup1 复制栈顶的第二个值
  • OP_insert2 复制栈顶值并插入到栈顶第三个位置
  • OP_insert3 复制栈顶值并插入到第四个位置
  • OP_insert4 复制栈顶值并插入到第五个位置
  • OP_perm3 交换第二和第三个值的位置
  • OP_rot3l 向左旋转栈顶前三个值
  • OP_rot4l 向左旋转栈顶前四个值
  • OP_rot5l 向左旋转栈顶前五个值
  • OP_rot3r 向右旋转栈顶前三个值
  • OP_perm4 将栈顶第二个值插入到第四个位置
  • OP_perm5 将栈顶第二个值插入到第五个位置
  • OP_swap 将栈顶前两个值互换
  • OP_swap2 将栈顶前四个值互换
  • OP_fclosure 获取常量池中的bytecode function,并创建对应的closure对象
  • OP_call0 有0个参数的函数调用
  • OP_call1 有1个参数的函数调用
  • OP_call2 有2个参数的函数调用
  • OP_call3 有3个参数的函数调用
  • OP_call 读取参数个数,调用函数
  • OP_tail_call 尾调用优化,调用函数
  • OP_call_constructor 调用构造函数
  • OP_call_method 调用方法函数
  • OP_tail_call_method 尾调用优化,调用方法函数
  • OP_array_from 创建array,参数为element值
  • OP_apply 函数apply调用
  • OP_return 函数返回
  • OP_return_undef 函数返回undefined
  • OP_check_ctor_return 检查构造函数的返回值
  • OP_check_ctor 检查构造函数使用new
  • OP_check_brand
  • OP_add_brand
  • OP_throw 抛出错误
  • OP_throw_error 抛出内置运行时错误
  • OP_eval
  • OP_apply_eval
  • OP_regexp 根据预编译创建正则表达式
  • OP_get_super 获取object对应的prototype对象
  • OP_import
  • OP_check_var 检查全局变量是否存在
  • OP_get_var_undef 获取全局变量,不抛出错误
  • OP_get_var 获取全局变量,抛出错误
  • OP_put_var 写全局变量,正常写
  • OP_put_var_init 写全局变量,lexically写
  • OP_put_var_strict 严格模式下写全局变量
  • OP_check_define_var 检查全局变量是否定义
  • OP_define_var 定义全局变量
  • OP_define_func 定义全局函数
  • OP_get_loc 获取函数内指定位置的局部变量
  • OP_put_loc 写入函数内指定位置的局部变量
  • OP_set_loc 写入函数内指定位置的局部变量,并Dup
  • OP_get_arg 获取指定位置的函数参数
  • OP_put_arg 写入指定位置的函数参数
  • OP_set_arg 写入指定位置的函数参数,并Dup
  • OP_get_var_ref 获取指定位置的闭包变量
  • OP_put_var_ref 写入指定位置的闭包变量
  • OP_set_var_ref 写入指定位置的闭包变量,并Dup
  • OP_get_var_ref_check 获取指定位置的闭包变量,并检查是否初始化
  • OP_put_var_ref_check 写入指定位置的闭包变量,并检查是否初始化
  • OP_put_var_ref_check_init 写入指定位置的闭包变量,并初始化
  • OP_set_loc_uninitialized 写入指定位置的闭包变量uninitialized值
  • OP_get_loc_check 获取函数指定位置的局部变量,并检查是否初始化
  • OP_put_loc_check 写入函数指定位置的局部变量,并检查是否初始化
  • OP_put_loc_check_init 写入函数指定位置的局部变量,并初始化
  • OP_close_loc 将函数内变量移入闭包变量
  • OP_make_loc_ref
  • OP_make_arg_ref
  • OP_make_var_ref_ref
  • OP_make_var_ref
  • OP_goto 跳转到指定偏移位置
  • OP_goto16 跳转到指定偏移位置,偏移量为16位
  • OP_goto8 跳转到指定偏移位置,偏移量为8位
  • OP_if_true 判断值是否为真值,若为真值,跳转到指定偏移位置
  • OP_if_false 判断值是否为假值,若为假值,跳转到指定偏移位置
  • OP_if_true8 偏移量为8位
  • OP_if_false8 偏移量为8位
  • OP_catch
  • OP_gosub 跳转到指定位置,加上偏移量
  • OP_ret
  • OP_for_in_start
  • OP_for_in_next
  • OP_for_of_start
  • OP_for_of_next
  • OP_for_await_of_start
  • OP_iterator_get_value_done
  • OP_iterator_check_object
  • OP_iterator_close
  • OP_iterator_close_return
  • OP_iterator_next
  • OP_iterator_call
  • OP_lnot 逻辑否运算符
  • OP_get_field 获取对象中指定的属性,并释放对象
  • OP_get_field2 获取对象中指定的属性,并压入栈顶
  • OP_put_field 设置对象中指定的属性
  • OP_private_symbol
  • OP_get_private_field 获取对象中的私有属性
  • OP_put_private_field 设置对象中的私有属性
  • OP_define_private_field 定义对象的私有属性
  • OP_define_field 定义对象的属性
  • OP_set_name 设置对象的name属性
  • OP_set_name_computed 设置对象的name属性,使用计算值
  • OP_set_proto 设置对象的prototype
  • OP_set_home_object
  • OP_define_method 定义对象的方法属性
  • OP_define_method_computed 定义对象的方法属性,函数名为计算值
  • OP_define_class 定义class
  • OP_define_class_computed 定义class,类名为计算值
  • OP_get_array_el 获取以索引访问的属性值,并释放对象
  • OP_get_array_el2 获取以索引访问的属性值,并push到栈顶上
  • OP_get_ref_value
  • OP_get_super_value
  • OP_put_array_el 写入以索引访问的属性值
  • OP_put_ref_value
  • OP_put_super_value
  • OP_define_array_el 定义以索引访问的属性值
  • OP_append 向数组中添加可迭代的值
  • OP_copy_data_properties
  • OP_add 对两个数进行相加
  • OP_add_loc 对两个数相加,其中一个使用索引到函数内局部变量
  • OP_sub 对两个数进行相减
  • OP_mul 对两个数进行相乘
  • OP_div 对两个数进行相除
  • OP_mod 对两个数做模运算
  • OP_pow 对两个数做幂运算 -