深刻V8引擎-AST(5)

懒得发首页了,有时候由于贴的代码太多会被下,并且这东西原本也只是对本身学习的记录,阅读体验极差,因此就本地自娱自乐的写着吧!html

因为是解析字符串,因此在开始以前介绍一下词法结构体中关于管理字符串类的属性。以前在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容量外的下标赋值,不知道会出现什么状况。ui

回到上一篇的结尾,因为匹配到单引号,因此会走ScanString方法,源码以下。this

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日了狗,主要做用就是预先判断一下当前解析的字符串是否合法,整个函数结构以下。spa

/**
 * 参数是一个匿名函数
 */
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_);
        });
    /**
     * 一、碰到第二个参数 说明没有符合条件的字符 直接返回结束符
     * 二、有符合条件的字符 把游标属性指向该字符的后一位 返回该字符
     */
    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也就是根据索引来找符合对应条件的值。也就是说,惟一须要讲解的就是字符串结束符的判断。code

涉及的新属性有两个,其中一个是映射数组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的容器则存储着对应的字符串。

相关文章
相关标签/搜索