2

引擎剖析:JS 中的字符串转数值(下)

 1 year ago
source link: https://me.ursb.me/archives/js-string-to-number-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

书接上文《引擎剖析:JS 中的字符串转数值(上)》,本文继续讲解利用位运算做数值转换的两种方式。

4. Double tilde (~~) Operator

ECMAScript (ECMA-262) Bitwise NOT Operator

Pasted%20image%2020220503113745.png

使用 ~ 运算符利用到了标准中的第 2 步,对被计算的值做类型转换,从而将字符串转成数值。这里我们关注这个环节具体是在引擎中的哪个步骤完成的。

4.1 V8 中的 BitwiseNot

首先看看 V8 中对一元运算符的判断:

[→ src/parsing/token.h]

static bool IsUnaryOp(Value op) { return base::IsInRange(op, ADD, VOID); }

定义在 ADD 和 VOID 范围内的 op,都是一元运算符,具体包括 (可见 [→ src/parsing/token.h]),其中 SUB 和 ADD 定义在二元运算符列表的末端,在 IsUnaryOp 中它们也会命中一元符的判断:

E(T, ADD, "+", 12)
E(T, SUB, "-", 12)
T(NOT, "!", 0)
T(BIT_NOT, "~", 0)
K(DELETE, "delete", 0)
K(TYPEOF, "typeof", 0)
K(VOID, "void", 0)

之后进入语法分析阶段,解析 AST 树的过程中,遇到一元运算符会做相应的处理,先调用 ParseUnaryOrPrefixExpression 之后构建一元运算符表达式 BuildUnaryExpression

[→ src/parsing/parser-base.h]

template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryExpression() {
  // UnaryExpression ::
  //   PostfixExpression
  //   'delete' UnaryExpression
  //   'void' UnaryExpression
  //   'typeof' UnaryExpression
  //   '++' UnaryExpression
  //   '--' UnaryExpression
  //   '+' UnaryExpression
  //   '-' UnaryExpression
  //   '~' UnaryExpression
  //   '!' UnaryExpression
  //   [+Await] AwaitExpression[?Yield]

  Token::Value op = peek();
  // 一元运算符处理
  if (Token::IsUnaryOrCountOp(op)) return ParseUnaryOrPrefixExpression();
  if (is_await_allowed() && op == Token::AWAIT) {
    // await 处理
    return ParseAwaitExpression();
  }
  return ParsePostfixExpression();
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryOrPrefixExpression() {
    //...

    //...
    // Allow the parser's implementation to rewrite the expression.
    return impl()->BuildUnaryExpression(expression, op, pos);
}

[→ src/parsing/parser.cc]

Expression* Parser::BuildUnaryExpression(Expression* expression,
                                         Token::Value op, int pos) {
  DCHECK_NOT_NULL(expression);
  const Literal* literal = expression->AsLiteral();
  if (literal != nullptr) {
    // !
    if (op == Token::NOT) {
      // Convert the literal to a boolean condition and negate it.
      return factory()->NewBooleanLiteral(literal->ToBooleanIsFalse(), pos);
    } else if (literal->IsNumberLiteral()) {
      // Compute some expressions involving only number literals.
      double value = literal->AsNumber();
      switch (op) {
        // +
        case Token::ADD:
          return expression;
        // -
        case Token::SUB:
          return factory()->NewNumberLiteral(-value, pos);
        // ~
        case Token::BIT_NOT:
          return factory()->NewNumberLiteral(~DoubleToInt32(value), pos);
        default:
          break;
      }
    }
  }
  return factory()->NewUnaryOperation(op, expression, pos);
}

如果字面量是数值型且一元运算符此刻不是 NOT(!),那么会把 Value 会转成 Number,如果是 BIT_NOT 再转成 INT32 进行取反运算。

4.2 JavaScriptCore 中的 BitwiseNot

同样在语法分析生成 AST 阶段,处理到 TILDE(~) 这个 token 后,创建表达式时会做类型转换的工作:

[→ Parser/Parser.cpp]

template <typename LexerType>
template <class TreeBuilder> TreeExpression Parser<LexerType>::parseUnaryExpression(TreeBuilder& context)
{
    //... 省略无关代码
     while (tokenStackDepth) {
        switch (tokenType) {
        //... 省略无关代码
        // ~
        case TILDE:
                expr = context.makeBitwiseNotNode(location, expr);
                break;
         // +
        case PLUS:
                expr = context.createUnaryPlus(location, expr);
                break;
        //... 省略无关代码
        }
    }
}

[→ parser/ASTBuilder.h]

ExpressionNode* ASTBuilder::makeBitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
{
    if (expr->isNumber())
        return createIntegerLikeNumber(location, ~toInt32(static_cast<NumberNode*>(expr)->value()));
    return new (m_parserArena) BitwiseNotNode(location, expr);
}

[→ parser/NodeConstructors.h]

inline BitwiseNotNode::BitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
        : UnaryOpNode(location, ResultType::forBitOp(), expr, op_bitnot)
{
}

[→ parser/ResultType.h]

static constexpr ResultType forBitOp()
{
    return bigIntOrInt32Type();
}

static constexpr ResultType bigIntOrInt32Type()
{
    return ResultType(TypeMaybeBigInt | TypeInt32 | TypeMaybeNumber);
}

4.3 QuickJS 中的 BitwiseNot

QuickJS 在语法分析阶段,遇到 ~ 这个 token 会调用 emit_op(s, OP_not)

[→ quickjs.c]

/* allowed parse_flags: PF_ARROW_FUNC, PF_POW_ALLOWED, PF_POW_FORBIDDEN */
static __exception int js_parse_unary(JSParseState *s, int parse_flags)
{
    int op;

    switch(s->token.val) {
    case '+':
    case '-':
    case '!':
    case '~':
    case TOK_VOID:
        op = s->token.val;
        if (next_token(s))
            return -1;
        if (js_parse_unary(s, PF_POW_FORBIDDEN))
            return -1;
        switch(op) {
        case '-':
            emit_op(s, OP_neg);
            break;
        case '+':
            emit_op(s, OP_plus);
            break;
        case '!':
            emit_op(s, OP_lnot);
            break;
        case '~':
            emit_op(s, OP_not);
            break;
        case TOK_VOID:
            emit_op(s, OP_drop);
            emit_op(s, OP_undefined);
            break;
        default:
            abort();
        }
        parse_flags = 0;
        break;
    //...
    }
    //...
    }
}

emit_op 会生成 OP_not 字节码操作符,并将源码保存在 fd->byte_code 里。

static void emit_op(JSParseState *s, uint8_t val)
{
    JSFunctionDef *fd = s->cur_func;
    DynBuf *bc = &fd->byte_code;

    /* Use the line number of the last token used, not the next token,
       nor the current offset in the source file.
     */
    if (unlikely(fd->last_opcode_line_num != s->last_line_num)) {
        dbuf_putc(bc, OP_line_num);
        dbuf_put_u32(bc, s->last_line_num);
        fd->last_opcode_line_num = s->last_line_num;
    }
    fd->last_opcode_pos = bc->size;
    dbuf_putc(bc, val);
}

int dbuf_putc(DynBuf *s, uint8_t c)
{
    return dbuf_put(s, &c, 1);
}

int dbuf_put(DynBuf *s, const uint8_t *data, size_t len)
{
    if (unlikely((s->size + len) > s->allocated_size)) {
        if (dbuf_realloc(s, s->size + len))
            return -1;
    }
    memcpy(s->buf + s->size, data, len);
    s->size += len;
    return 0;
}

QuickJS 解释执行的函数是 JS_EvalFunctionInternal,其会调用 JS_CallFree 进行字节码的解释执行,其核心逻辑是调用的 JS_CallInternal 函数。

/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
                               JSValueConst this_obj, JSValueConst new_target,
                               int argc, JSValue *argv, int flags)
{
    JSRuntime *rt = caller_ctx->rt;
    JSContext *ctx;
    JSObject *p;
    JSFunctionBytecode *b;
    JSStackFrame sf_s, *sf = &sf_s;
    const uint8_t *pc;
    // ...省略无关代码

    for(;;) {
        int call_argc;
        JSValue *call_argv;
        SWITCH(pc) {
        // ...
        CASE(OP_not):
        {
            JSValue op1;
            op1 = sp[-1];
            // 如果是整型
            if (JS_VALUE_GET_TAG(op1) == JS_TAG_INT) {
                sp[-1] = JS_NewInt32(ctx, ~JS_VALUE_GET_INT(op1));
            // 如果不是整型
            } else {
                if (js_not_slow(ctx, sp))
                    goto exception;
            }
        }
        BREAK;
        // ...
    }
    // ...
}

可见,解析到 OP_not 时, 如果是整型就直接取反,否则就调用 js_not_slow

static no_inline int js_not_slow(JSContext *ctx, JSValue *sp)
{
    int32_t v1;

    if (unlikely(JS_ToInt32Free(ctx, &v1, sp[-1]))) {
        sp[-1] = JS_UNDEFINED;
        return -1;
    }
    sp[-1] = JS_NewInt32(ctx, ~v1);
    return 0;
}

js_not_slow 会尝试转整型,转不了就转 -1,转的了就转整型后取反。JS_ToInt32Free 转换逻辑如下:

/* return (<0, 0) in case of exception */
static int JS_ToInt32Free(JSContext *ctx, int32_t *pres, JSValue val)
{
 redo:
    tag = JS_VALUE_GET_NORM_TAG(val);
    switch(tag) {
    case JS_TAG_INT:
    case JS_TAG_BOOL:
    case JS_TAG_NULL:
    case JS_TAG_UNDEFINED:
        ret = JS_VALUE_GET_INT(val);
        break;
        // ...
    default:
        val = JS_ToNumberFree(ctx, val);
        if (JS_IsException(val)) {
            *pres = 0;
            return -1;
        }
        goto redo;
    }
    *pres = ret;
    return 0;
}

对于字符串,会走到 JS_ToNumberFree,之后调用 JS_ToNumberHintFree,涉及到字符串处理的核心逻辑如下:

static JSValue JS_ToNumberHintFree(JSContext *ctx, JSValue val,
                                   JSToNumberHintEnum flag)
{
    uint32_t tag;
    JSValue ret;

 redo:
    tag = JS_VALUE_GET_NORM_TAG(val);
    switch(tag) {
    // ...省略无关逻辑
    case JS_TAG_STRING:
        {
            const char *str;
            const char *p;
            size_t len;

            str = JS_ToCStringLen(ctx, &len, val);
            JS_FreeValue(ctx, val);
            if (!str)
                return JS_EXCEPTION;
            p = str;
            p += skip_spaces(p);
            if ((p - str) == len) {
                ret = JS_NewInt32(ctx, 0);
            } else {
                int flags = ATOD_ACCEPT_BIN_OCT;
                ret = js_atof(ctx, p, &p, 0, flags);
                if (!JS_IsException(ret)) {
                    p += skip_spaces(p);
                    if ((p - str) != len) {
                        JS_FreeValue(ctx, ret);
                        ret = JS_NAN;
                    }
                }
            }
            JS_FreeCString(ctx, str);
        }
        break;
    // ...省略无关逻辑
    }
    // ...省略无关逻辑
}

可以转化的用 JS_NewInt32 去处理,否则返回 NaN。

5. Unary Operator (+)

ECMAScript (ECMA-262) Unary Plus Operator

Pasted%20image%2020220503114251.png

一元运算符加号是笔者最喜欢用的一种字符串转数值的方式,标准中它没有什么花里胡哨的、非常简介明了,就是用来做数值类型转换的。

5.1 V8 中的 UnaryPlus

语法分析阶段同 Double tilde (~~) Operator,此处不再赘述。

5.2 JavaScriptCore 中的 UnaryPlus

语法分析阶段同 Double tilde (~~) Operator,此处不再赘述。

5.3 QuickJS 中的 UnaryPlus

语法分析阶段同 Double tilde (~~) Operator,此处不再赘述。最后依然走到 JS_CallInternal

[→ quickjs.c]

/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
                               JSValueConst this_obj, JSValueConst new_target,
                               int argc, JSValue *argv, int flags)
{
    JSRuntime *rt = caller_ctx->rt;
    JSContext *ctx;
    JSObject *p;
    JSFunctionBytecode *b;
    JSStackFrame sf_s, *sf = &sf_s;
    const uint8_t *pc;
    // ...省略无关代码

    for(;;) {
        int call_argc;
        JSValue *call_argv;
        SWITCH(pc) {
        // ...
        CASE(OP_plus):
            {
                JSValue op1;
                uint32_t tag;
                op1 = sp[-1];
                tag = JS_VALUE_GET_TAG(op1);
                if (tag == JS_TAG_INT || JS_TAG_IS_FLOAT64(tag)) {
                } else {
                    if (js_unary_arith_slow(ctx, sp, opcode))
                        goto exception;
                }
                BREAK;
            }
        // ...省略无关代码
        }
    }
    // ...省略无关代码
}

可以发现当操作数是 Int 或 Float 时,就直接不处理,和标准中规范的一致。而其他情况就调用 js_unary_arith_slow,若调用过程中遇到异常就走异常逻辑:

static no_inline __exception int js_unary_arith_slow(JSContext *ctx, JSValue *sp, OPCodeEnum op)
{
    JSValue op1;
    double d;

    op1 = sp[-1];
    if (unlikely(JS_ToFloat64Free(ctx, &d, op1))) {
        sp[-1] = JS_UNDEFINED;
        return -1;
    }
    switch(op) {
    case OP_inc:
        d++;
        break;
    case OP_dec:
        d--;
        break;
    case OP_plus:
        break;
    case OP_neg:
        d = -d;
        break;
    default:
        abort();
    }
    sp[-1] = JS_NewFloat64(ctx, d);
    return 0;
}

这里的 JS_ToFloat64Free 的内部处理逻辑和和 4.3 时的 JS_ToFloat64Free 一样,不再赘述。js_unary_arith_slow 处理完数值转换之后,若运算符是一元运算加号,则直接返回;否则还会根据运算符再做相应的运算处理,如自增符还需要+1 等。


至此,我们讲解了以下 5 个方法在解释器中的具体实现:

  1. parseInt()
  2. parseFloat()
  3. Number()
  4. Double tilde (~~) Operator
  5. Unary Operator (+)

除却以上 5 个数值转换方法之外,还有以下 4 个方法,因篇幅问题本文暂且不再详述:

  • Math.floor()
  • Multiply with number
  • The Signed Right Shift Operator(>>)
  • The Unsigned Right Shift Operator(>>>)

字符串转数值各有优劣,使用者可根据自己的需要进行选用,以下是我个人总结的一些经验:

如果返回值只要求整形:

  • 追求代码简洁和执行效率,对输入值有一定的把握(无需防御),优先选用 Unary Operator (+)
  • 对输入值没有把握,需要做防御式编程,使用 parseInt()
  • 需要支持 BigInt, 优先考虑使用 Number() ;如果用 Double tilde (~~) Operator,需要注意 31 位问题。

如果返回值要求浮点型:

  • 追求代码简洁和执行效率,对输入值有一定的把握(无需防御),优先选用 Unary Operator (+)
  • 对输入值没有把握,需要做防御式编程,使用 parseFloat()
  • 需要支持 BigInt,使用 parseFloat()

本文由 Airing 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: May 3, 2022 at 06:52 am


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK