QuickJS 数字字面量解析

背景

QuickJS是Fabrice Bellard在2019年发布的一款支持ES6的JavaScript引擎。Fabrice Bellard是一位富有传奇经历的大佬。自己独立开发、发起和维护的耀眼明星项目有:FFmpeg、TCC、QEMU、JSLinux和LTEENB等等。FFmpeg是一个开源音视频处理库,提供了简单便捷统一的音视频处理接口,将广大的音视频开发程序员从音视频开发苦海中解放出来。QEMU是一个模拟器框架,绝大多数PC上的安卓模拟器都是基于它开发出来的。TCC是x86架构下,世界上最快的C语言编译器。JSLinux更是神,直接将linux系统运行到浏览器中。你以为这就完了?他一个人用10个月的时间,开发了一个可以在PC机上运行的4G LTE/5G NR基站程序。仅仅10个月啊,我连协议都看不完😓。毫不夸张的说,可能他一个人至少养活了全球上万个程序员。

QuickJS运行JavaScript原理

QuickJS在运行JavaScript源码时,先解析源码,源码解析成token,再根据生成字节码。字节码由操作码、操作数和标签组成。然后再对字节码进行优化。最后,再遍历字节码,根据操作码和操作数,执行操作和计算。
解析源码依赖的是Token。根据TC39制定的ECMAScript词法定义,将ECMAScript通用Token分成了以下几种

  • IndentifierName
  • PraviteIndentifier
  • Punctuator
  • NumbericLiteral
  • StringLiteral
  • Template

数字字面量

我们这篇文章的目的介绍大佬如何解析NumbericLiteral的。NumbericLiteral的意思是数字字面量。可能有同学学编程时,没有注意到字面量的定义,有点不太理解含义。字面量(literal)是用于表达一个固定值的表示法,又叫常量。举个例子:

let x = 10
let y = "hello,world"
console.log("nice~~~")

这个例子中,有两个变量分别是x和y。例子中还有三个字面量,其中有一个数字字面量、两个字符串字面量,分别是:10,“hello,world”, “nice~~~”。

  • 这里的数字就是数学意义上的数字。
  • 数字字面量区分:整数字面量、浮点数字面量(小数)、特殊值。
  • 书写时直接书写字面量,不需要添加任何辅助符号。

科学计数法

var x = 123e5;    // 12300000
var z = 123e-5;   // 0.00123

JavaScript 数值始终是 64 位的浮点数,JavaScript 数值始终以双精度浮点数来存储,根据国际 IEEE 754 标准。

整数

整数字面量写法区分进制。可以被表示成十进制、八进制以及十六进制。在进行算术计算时或者参与程序,所有八进制和十六进制的数字都会被转换成十进制。

十进制

十进制最基本的数值字面量格式,基数为10。逢十进一,每个位数只能是0-9之间的数字。

八进制

八进制字面量必须带前导0、0O、0o。基数为8。逢八进一,每个位数只能是0-7之间的数字。(如果以0开头,每个位数上有超过0-7之间的数字出现,也就是8/9,强制忽视前面的0,直接将后面数字当做十进制)
下面是八进制转十进制的方法:
0O011 = 0 * 8^2 + 1 * 8^1 + 1 * 8^0 = 9

十六进制

十六进制字面量必须带前缀0x和0X。基数为16。逢十六进一,每个位数只能是数字0-9、字母a-f或A~F。
下面是十六进制转十进制的方法:
0x01000 = 0 * 16^0 + 0 * 16^1 + 0 * 16^2 +1 * 16^3 + 0 * 16^4 = 4096

浮点数

数学概念中的小数,浮点数不区分进制,所有的浮点数都是十进制下的数字。
注意:如果浮点数是大于0小于1的,可以省略小数点前面的0不写。
浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数。
例如:0.1+0.2;结果不是0.3,而是:0.30000000000000004

Infinity 无穷

Infinity (或 -Infinity)是 JavaScript 在计算数时超出最大可能数范围时返回的值。
Infinity本身就是一个数字。

  • 最小值:Number.MIN_VALUE,这个值为: 5e-324
  • 最大值:Number.MAX_VALUE,这个值为: 1.7976931348623157e+308
  • 无穷大:Infinity
  • 无穷小:-Infinity

NaN 非数值

  • NaN:not a number表示不是一个正常的数,但是还是一个 Number 类型的数字。这个数字 没办法用前面的表示方法表示。
  • NaN与任何值都不相等,包括他本身。
  • isNaN():判断一个数据是不是一个NaN。

补课

ASCII

为了让更多基础不强的同学能看懂,在此补下课。关于ASCII,引用下面的百度定义。我们存储类似英文字母、阿拉伯数字、英文标点符号等字符串到文本中,一般都遵循ASCII标准。比如,我们存储字符’A’到文本中,存储的不是字符‘A’,而是一个数字,这个数字就是ASCII表中,字符’A’对应的数字-- 65。
ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
ASCII编码

将数字字面量转化成数字

QuickJS将JavaScript代码先当作字符串读入内存。然后逐字节遍历这个字符串,当判断某一个字节的在字符 ‘0’ 和字符 ‘9’ 之间时,认为识别到了数字字面量的起始位置。然后将该字符串,转化成数字。
比如上面的例子中的第一行,程序从第一个字符 ‘l’ ('let’的第一个字符l)开始遍历,直到第9个字符,发现是字符 ‘1’ , 正好处于字符 ‘0’ 和字符 ‘9’ 之间。这时,程序认为这可能是一个数字字面量的开始。需要注意的是 ’ ’ – 空白也是一个字符,占用一个字节长度。然后继续遍历,直到下一个空字符。这很好理解,因为数字字面量是连续的,中间不能包含有空字符。
如此,我们便得到了一个完整的数字字面量 – 10。此时,它还是个字符串。如何将它解析成数字呢?看看大佬是如何做的。js_atof是将字符串转化成数字的函数,我们先放出来它的源码。

#define ATOD_INT_ONLY        (1 << 0)
/* accept Oo and Ob prefixes in addition to 0x prefix if radix = 0 */
#define ATOD_ACCEPT_BIN_OCT  (1 << 2)
/* accept O prefix as octal if radix == 0 and properly formed (Annex B) */
#define ATOD_ACCEPT_LEGACY_OCTAL  (1 << 4)
/* accept _ between digits as a digit separator */
#define ATOD_ACCEPT_UNDERSCORES  (1 << 5)
/* allow a suffix to override the type */
#define ATOD_ACCEPT_SUFFIX    (1 << 6) 
/* accept -0x1 */
#define ATOD_ACCEPT_PREFIX_AFTER_SIGN (1 << 10)

static JSValue js_atof(JSContext *ctx, const char *str, const char **pp,
                       int radix, int flags)
{
    const char *p, *p_start;
    int sep, is_neg;
    BOOL is_float, has_legacy_octal;
    int atod_type = flags & ATOD_TYPE_MASK;
    char buf1[64], *buf;
    int i, j, len;
    BOOL buf_allocated = FALSE;
    JSValue val;
    
    /* optional separator between digits */
    sep = (flags & ATOD_ACCEPT_UNDERSCORES) ? '_' : 256;
    has_legacy_octal = FALSE;
    
    p = str;
    p_start = p;
    is_neg = 0;
    if (p[0] == '+') {
        p++;
        p_start++;
        if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN))
            goto no_radix_prefix;
    } else if (p[0] == '-') {
        p++;
        p_start++;
        is_neg = 1;
        if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN))
            goto no_radix_prefix;
    }
    if (p[0] == '0') {
        if ((p[1] == 'x' || p[1] == 'X') &&
            (radix == 0 || radix == 16)) {
            p += 2;
            radix = 16;
        } else if ((p[1] == 'o' || p[1] == 'O') &&
                   radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) {
            p += 2;
            radix = 8;
        } else if ((p[1] == 'b' || p[1] == 'B') &&
                   radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) {
            p += 2;
            radix = 2;
        } else if ((p[1] >= '0' && p[1] <= '9') &&
                   radix == 0 && (flags & ATOD_ACCEPT_LEGACY_OCTAL)) {
            int i;
            has_legacy_octal = TRUE;
            sep = 256;
            for (i = 1; (p[i] >= '0' && p[i] <= '7'); i++)
                continue;
            if (p[i] == '8' || p[i] == '9')
                goto no_prefix;
            p += 1;
            radix = 8;
        } else {
            goto no_prefix;
        }
        /* there must be a digit after the prefix */
        if (to_digit((uint8_t)*p) >= radix)
            goto fail;
    no_prefix: ;
    } else {
 no_radix_prefix:
        if (!(flags & ATOD_INT_ONLY) &&
            (atod_type == ATOD_TYPE_FLOAT64 ||
             atod_type == ATOD_TYPE_BIG_FLOAT) &&
            strstart(p, "Infinity", &p)) {
            {
                double d = 1.0 / 0.0;
                if (is_neg)
                    d = -d;
                val = JS_NewFloat64(ctx, d);
            }
            goto done;
        }
    }
    if (radix == 0)
        radix = 10;
    is_float = FALSE;
    p_start = p;//计算有效数字开始的地方,有效数字指的是出去进制符号信息的数字,如:+0x988 中 988 就是有效数字
    
    //跳过满足进制要求的字符
    while (to_digit((uint8_t)*p) < radix
           ||  (*p == sep && (radix != 10 ||
                              p != p_start + 1 || p[-1] != '0') &&
                to_digit((uint8_t)p[1]) < radix)) {
        p++;
    }
    if (!(flags & ATOD_INT_ONLY)) {
        // 判断是否是浮点数字中出现的小数点
        if (*p == '.' && (p > p_start || to_digit((uint8_t)p[1]) < radix)) {
            is_float = TRUE;
            p++;
            if (*p == sep)
                goto fail;
            //跳过满足进制要求的字符
            while (to_digit((uint8_t)*p) < radix ||
                   (*p == sep && to_digit((uint8_t)p[1]) < radix))
                p++;
        }
        //科学计数法
        if (p > p_start &&
            (((*p == 'e' || *p == 'E') && radix == 10) ||
             ((*p == 'p' || *p == 'P') && (radix == 2 || radix == 8 || radix == 16)))) {
            const char *p1 = p + 1;
            is_float = TRUE;
            if (*p1 == '+') {
                p1++;
            } else if (*p1 == '-') {
                p1++;
            }
            if (is_digit((uint8_t)*p1)) {
                p = p1 + 1;
                //遍历跳过所有数字
                while (is_digit((uint8_t)*p) || (*p == sep && is_digit((uint8_t)p[1])))
                    p++;
            }
        }
    }
    if (p == p_start)
        goto fail;

    buf = buf1;
    buf_allocated = FALSE;
    len = p - p_start;//得到有效数字部分字符串长度
    //看看buf够不够用,不够就重新申请
    if (unlikely((len + 2) > sizeof(buf1))) {
        buf = js_malloc_rt(ctx->rt, len + 2); /* no exception raised */
        if (!buf)
            goto mem_error;
        buf_allocated = TRUE;
    }
    // 移除分隔符和进制前缀
    /* remove the separators and the radix prefixes */
    j = 0;
    //加上负号
    if (is_neg)
        buf[j++] = '-';
    //移除分隔符
    for (i = 0; i < len; i++) {
        if (p_start[i] != '_')
            buf[j++] = p_start[i];
    }
    //截断字符串
    buf[j] = '\0';
    
    {
        double d;
        (void)has_legacy_octal;
        //非十进制小数不符合规则
        if (is_float && radix != 10)
            goto fail;
        //将字符串转化成double
        d = js_strtod(buf, radix, is_float);
        //封装成JSValue
        val = JS_NewFloat64(ctx, d);
    }

done:
    if (buf_allocated)
        js_free_rt(ctx->rt, buf);
    if (pp)
        *pp = p;
    return val;
 fail:
    val = JS_NAN;
    goto done;
 mem_error:
    val = JS_ThrowOutOfMemory(ctx);
    goto done;
}

代码很简洁。这个函数的参数有五个,为了简化难度,我们只关注其中的两个即可,他们是:const char *str, int radix。第一个参数是待解析的字符串,第二个参数是数字的进制。
进入程序之后,首先判断是否支持数字表达式中的下划线分割符,比如“9_9_9”。
接着判断字面量中是否存在符号。如果存在 ‘-’ ,则表明这个数字是负数。其中,如果设置了允许在符号之后接受前缀,就判断进制。如果不允许在符号之后接受进制标签,就跳过进制判断。
然后,判断数字的进制:二进制、八进制、十六进制或十进制(默认)。判断的方式是:先看看符号后一位(如果有)是不是 ‘0’ ,如果是0,就判断第二位。如果第二位是 ‘x’ 或 ‘X’,则表明是十六进制。如果第二位是 ‘b’ 或 ‘B’ ,则表明是二进制。八进制的判断稍微复杂一些,首先要区分是否是js是否运行在严苛模式,如果是,不允许以八进制表示字面量。如果不是,判断第二位是否为 ‘o’ 或 ‘O’,如果是,就说明是八进制。如果不是,再判断是否支持老式八进制表达式。先前版本的八进制表达时是以 ‘0’ 开头,后面是其它数字,没有进制符号。当然,十进制表示时,也有可能是以0开始的。那么如何区分两种进制呢?遍历这个数字字面量,判断这个字符串中是否包含字符 ‘8’ 和 ‘9’,如果是,那么这个数就是十进制,否则是八进制。因为八进制字面量中不可能存在 ‘8’ 和 ‘9’,两个字符。最后,需要再确认下hex进制标识符后面一个字符是否是符合进制的数字。比如:如果是十进制,只能在 ‘0’ 到 ‘9’ 中间;如果是十六进制,只能在 ‘0’ 到 ‘F’ 之间。
如果数字不是以 ‘0’ 开头,或者有符号标签同时不支持进制标签,就会判断数字是否是Infinity。如果是就,直接将 1除以0 得到正无穷大,如果前面判断是负的,就直接加上负号得到无穷小。最后,封装成JSValue,返回。如果不是Infinity,则继续下面的处理。
在进入转换数字之前,需要再次确认下字面量中,是否所有的数字都满足对应进制的要求。再移除掉字符串中的分隔符和进制标识符。
然后继续遍历字符串,直到字符不是数字为止。考虑到数字字面了可能存在小数点和科学计数法表达式。如果发现存在小数点,说明这个数字是个浮点型数字。之后,发现需要分别判断并继续遍历小数点或科学计数法后面的数字。
得到完整有效字面量字符串之后,调用 js_strtod() 函数来将字面量转换成double数字。这个函数的代码如下:

/* XXX: remove */
static double js_strtod(const char *p, int radix, BOOL is_float)
{
    double d;
    int c;
    
    if (!is_float || radix != 10) {
        uint64_t n_max, n;
        int int_exp, is_neg;
        
        is_neg = 0;
        if (*p == '-') {
            is_neg = 1;
            p++;
        }

        /* skip leading zeros */
        while (*p == '0')
            p++;
        n = 0;
        if (radix == 10)
            n_max = ((uint64_t)-1 - 9) / 10; /* most common case */
        else
            n_max = ((uint64_t)-1 - (radix - 1)) / radix;
        /* XXX: could be more precise */
        int_exp = 0;
        while (*p != '\0') {
            c = to_digit((uint8_t)*p);
            if (c >= radix)
                break;
            if (n <= n_max) {
                n = n * radix + c;
            } else {
                int_exp++;
            }
            p++;
        }
        d = n;
        if (int_exp != 0) {
            d *= pow(radix, int_exp);
        }
        if (is_neg)
            d = -d;
    } else {
        d = strtod(p, NULL);
    }
    return d;
}

如果是十进制数整数,直接调用c标准库中的strtod直接将字符串转换成double类型数字。如果不是十进制数字整数或者是其它进制数字,会将其转换成十进制数字。

其它函数

to_digit

将字符串转成数字(按照ASCII表看就会明白)

static inline int to_digit(int c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    else if (c >= 'A' && c <= 'Z')
        return c - 'A' + 10;
    else if (c >= 'a' && c <= 'z')
        return c - 'a' + 10;
    else
        return 36;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值