深入V8引擎-AST(5)
懒得发首页了,有时候因为贴的代码太多会被下,而且这东西本来也只是对自己学习的记录,阅读体验极差,所以就本地自娱自乐的写着吧!
由于是解析字符串,所以在开始之前介绍一下词法结构体中关于管理字符串类的属性。之前在tokendesc中,有两个属性,如下。
/** * 词法结构体 * 每一个tokendesc代表单独一段词法 */ struct tokendesc { /** * 字符串词法相关 */ literalbuffer literal_chars; literalbuffer raw_literal_chars; // ... }
当时没有详细讲,主要也是比较麻烦,在这里介绍一下该类。
class literalbuffer final { public: /** * 根据字符unicode数值判断是单字节还是双字节字符 */ void addchar(uc32 code_unit) { if (is_one_byte()) { if (code_unit <= static_cast<uc32>(unibrow::latin1::kmaxchar)) { addonebytechar(static_cast<byte>(code_unit)); return; } converttotwobyte(); } addtwobytechar(code_unit); } private: /** * 配置 * constexpr int mb = kb * kb; constexpr int kb = 1024; */ static const int kinitialcapacity = 16; static const int kgrowthfactor = 4; static const int kmaxgrowth = 1 * mb; /** * 向容器加字符 */ void addonebytechar(byte one_byte_char) { if (position_ >= backing_store_.length()) expandbuffer(); backing_store_[position_] = one_byte_char; position_ += konebytesize; } /** * 容器扩容 * 初始至少有64的容量 根据需要扩容 * 会生成一个新容量的vector 把数据复制过去并摧毁老的容器 */ void literalbuffer::expandbuffer() { int min_capacity = max(kinitialcapacity, backing_store_.length()); vector<byte> new_store = vector<byte>::new(newcapacity(min_capacity)); if (position_ > 0) { memcopy(new_store.begin(), backing_store_.begin(), position_); } backing_store_.dispose(); backing_store_ = new_store; } /** * 扩容算法 * min_capacity代表容器最小所需容量 * (1024 * 1024) / 3 是一个阈值 * 小于该值容量以4倍的速度扩张 大于该值容量直接写死 */ int literalbuffer::newcapacity(int min_capacity) { return min_capacity < (kmaxgrowth / (kgrowthfactor - 1)) ? min_capacity * kgrowthfactor : min_capacity + kmaxgrowth; } /** * vector容器用来装字符 * potions_根据单/双字符类型影响length的计算 */ vector<byte> backing_store_; int position_; bool is_one_byte_; };
其实原理非常简单,用一个vector容器去装字符,如果容量不够,会进行扩张。
暂时不管双字节字符(比如中文),所以需要关注的属性和方法就是上面的那些,有一个地方可以关注一下,就是扩容。根据扩容机制,初始会有16 * 4的容量,当所需容量大到一定程度,会写死,这里来计算一下写死的最大容量。
/** * 计算 kmaxgrowth = 1024 * 1024 = 1048576 * 得到阈值 (kmaxgrowth / (kgrowthfactor - 1) = 1048576 / (4 - 1) = 349525.333 * 而未达到阈值前容器容量会从16开始每次乘以4 如下 * 64 256 1024 4096 16384 65536 262144 1048576 * 当扩容第7次时才出现比阈值大的数 这个值恰好等于1mb 因此容器容量最大值就是2mb */
单个字符串的解析长度原来是有上限的,最大为2mb,长度约为200万,此时会向vector容量外的下标赋值,不知道会出现什么情况。
回到上一篇的结尾,由于匹配到单引号,所以会走scanstring方法,源码如下。
token::value scanner::scanstring() { uc32 quote = c0_; /** * 初始化 */ next().literal_chars.start(); while (true) { /** * 对字符串的结尾预检测 */ advanceuntil([this](uc32 c0) { // ... }); /** * 遇到‘\’直接步进 * 后面如果直接是字符串结尾标识符 判定为非法 */ while (c0_ == '\\') { advance(); if (v8_unlikely(c0_ == kendofinput || !scanescape<false>())) { return token::illegal; } } /** * 又遇到了同一个字符串标识符 * 说明字符串解析完成 */ if (c0_ == quote) { advance(); return token::string; } /** * 没有合拢的字符串 返回非法标记 */ if (v8_unlikely(c0_ == kendofinput || unibrow::isstringliterallineterminator(c0_))) { return token::illegal; } // 向vector里面塞一个字符 addliteralchar(c0_); } }
总的来说还是比较简单的,正常步进是初始化用过的advance。代码中有一个方法叫advanceuntil,从函数名判断是一个预检函数。这个方法调用的结构非常奇怪,c++语法我也是tm日了狗,主要作用就是预先判断一下当前解析的字符串是否合法,整个函数结构如下。
/** * 参数是一个匿名函数 */ advanceuntil([this](uc32 c0) { // unicode大于127的特殊字符 if (v8_unlikely(static_cast<uint32_t>(c0) > kmaxascii)) { /** * 检测是否是换行符 * \r\n以及\n */ if (v8_unlikely(unibrow::isstringliterallineterminator(c0))) { return true; } addliteralchar(c0); return false; } /** * 检查是否是字符串结束符 */ uint8_t char_flags = character_scan_flags[c0]; if (mayterminatestring(char_flags)) return true; addliteralchar(c0); return false; }); /** * 这个方法会对c0_进行赋值 */ void advanceuntil(functiontype check) { c0_ = source_->advanceuntil(check); } template <typename functiontype> v8_inline uc32 advanceuntil(functiontype check) { while (true) { /** * 从游标位置到结尾搜索符合条件的字符 */ auto next_cursor_pos = std::find_if(buffer_cursor_, buffer_end_, [&check](uint16_t raw_c0_) { uc32 c0_ = static_cast<uc32>(raw_c0_); return check(c0_); }); /** * 1、碰到第二个参数 说明没有符合条件的字符 直接返回结束符 * 2、有符合条件的字符 把游标属性指向该字符的后一位 返回该字符 */ if (next_cursor_pos == buffer_end_) { buffer_cursor_ = buffer_end_; if (!readblockchecked()) { buffer_cursor_++; return kendofinput; } } else { buffer_cursor_ = next_cursor_pos + 1; return static_cast<uc32>(*next_cursor_pos); } } }
这里的调用方式比较邪门,其实就是js的高阶函数,函数作为参数传入函数,比较核心的就是find_if方法与函数参数,这里就不讲std的方法了,用js翻译一下,不然看起来实在太痛苦。
const callback = (str) => isstringliterallineterminator(str); const advanceuntil = (callback) => { let tararea = buffer_.slice(buffer_cursor_, buffer_end_); let taridx = tararea.findidx(v => callback(v)); if(taridx === - 1) return '非法字符串'; buffer_cursor_ = taridx + 1; c0_ = buffer_[taridx]; }
就是这么简单,变量直接对应,逻辑的话也就上面这些,find_if也就是根据索引来找符合对应条件的值。也就是说,唯一需要讲解的就是字符串结束符的判断。
涉及的新属性有两个,其中一个是映射数组character_scan_flags,另外一个是mayterminatestring方法,两者其实是一个东西,可以放一起看。
inline bool mayterminatestring(uint8_t scan_flags) { return (scan_flags & static_cast<uint8_t>(scanflags::kstringterminator)); } /** * 字符扫描标记 */ enum class scanflags : uint8_t { kterminatesliteral = 1 << 0, // "cannot" rather than "can" so that this flag can be ored together across // multiple characters. kcannotbekeyword = 1 << 1, kcannotbekeywordstart = 1 << 2, kstringterminator = 1 << 3, kidentifierneedsslowpath = 1 << 4, kmultilinecommentcharacterneedsslowpath = 1 << 5, }; /** * 映射表 * 对字符的可能性进行分类 */ static constexpr const uint8_t character_scan_flags[128] = { #define call_get_scan_flags(n) getscanflags(n), int_0_to_127_list(call_get_scan_flags) #undef call_get_scan_flags };
首先可以看出,character_scan_flags也是类似于之前那个unicode与ascii的表,对所有字符做一个映射,映射的值就是那个枚举类型,一个字符可能对应多个可能性。这里的计算方法可以参照我之前那篇利用枚举与位运算做配置,需要哪个属性,就用对应的枚举与字符映射值做与运算。
这个映射表的生成比较简单粗暴,会对每一个字符做6重或运算生成一个数,目前只看字符串终止符那块。
constexpr uint8_t getscanflags(char c) { return /** 1 */ | /** 2 */ | /** 3 */ | // possible string termination characters. ((c == '\'' || c == '"' || c == '\n' || c == '\r' || c == '\\') ? static_cast<uint8_t>(scanflags::kstringterminator) : 0) | /** 5 */ | /** 6 */ }
也就是说,当前字符是单双引号、换行与反斜杠时,会被认定可能是一个字符串的结尾。
回到编译字符串'hello',由于在字符结束之前,就存在另一个单引号,所以这个符号被认为可能是结束符号赋值给了c0_,stream类的游标也直接移到了那个位置。至于中间的h、e、l、l、o5个字符,因为不存在任何特殊性,所以在最后的addliteralchar方法中被添加进了容器中。
结束后,整个函数正常返回token::string作为词法结构体的类型,结构体的literal_chars的容器则存储着对应的字符串。
上一篇: js 函数节流
下一篇: CSS的再深入2(更新中···)