深入V8引擎-AST(5)

比眉伴天荒 2021-11-16 11:32 453阅读 0赞

懒得发首页了,有时候因为贴的代码太多会被下,而且这东西本来也只是对自己学习的记录,阅读体验极差,所以就本地自娱自乐的写着吧!

由于是解析字符串,所以在开始之前介绍一下词法结构体中关于管理字符串类的属性。之前在TokenDesc中,有两个属性,如下。

  1. /**
  2. * 词法结构体
  3. * 每一个TokenDesc代表单独一段词法
  4. */
  5. struct TokenDesc {
  6. /**
  7. * 字符串词法相关
  8. */
  9. LiteralBuffer literal_chars;
  10. LiteralBuffer raw_literal_chars;
  11. // ...
  12. }

当时没有详细讲,主要也是比较麻烦,在这里介绍一下该类。

  1. class LiteralBuffer final {
  2. public:
  3. /**
  4. * 根据字符Unicode数值判断是单字节还是双字节字符
  5. */
  6. void AddChar(uc32 code_unit) {
  7. if (is_one_byte()) {
  8. if (code_unit <= static_cast<uc32>(unibrow::Latin1::kMaxChar)) {
  9. AddOneByteChar(static_cast<byte>(code_unit));
  10. return;
  11. }
  12. ConvertToTwoByte();
  13. }
  14. AddTwoByteChar(code_unit);
  15. }
  16. private:
  17. /**
  18. * 配置
  19. * constexpr int MB = KB * KB; constexpr int KB = 1024;
  20. */
  21. static const int kInitialCapacity = 16;
  22. static const int kGrowthFactor = 4;
  23. static const int kMaxGrowth = 1 * MB;
  24. /**
  25. * 向容器加字符
  26. */
  27. void AddOneByteChar(byte one_byte_char) {
  28. if (position_ >= backing_store_.length()) ExpandBuffer();
  29. backing_store_[position_] = one_byte_char;
  30. position_ += kOneByteSize;
  31. }
  32. /**
  33. * 容器扩容
  34. * 初始至少有64的容量 根据需要扩容
  35. * 会生成一个新容量的vector 把数据复制过去并摧毁老的容器
  36. */
  37. void LiteralBuffer::ExpandBuffer() {
  38. int min_capacity = Max(kInitialCapacity, backing_store_.length());
  39. Vector<byte> new_store = Vector<byte>::New(NewCapacity(min_capacity));
  40. if (position_ > 0) {
  41. MemCopy(new_store.begin(), backing_store_.begin(), position_);
  42. }
  43. backing_store_.Dispose();
  44. backing_store_ = new_store;
  45. }
  46. /**
  47. * 扩容算法
  48. * min_capacity代表容器最小所需容量
  49. * (1024 * 1024) / 3 是一个阈值
  50. * 小于该值容量以4倍的速度扩张 大于该值容量直接写死
  51. */
  52. int LiteralBuffer::NewCapacity(int min_capacity) {
  53. return min_capacity < (kMaxGrowth / (kGrowthFactor - 1))
  54. ? min_capacity * kGrowthFactor
  55. : min_capacity + kMaxGrowth;
  56. }
  57. /**
  58. * Vector容器用来装字符
  59. * potions_根据单/双字符类型影响length的计算
  60. */
  61. Vector<byte> backing_store_;
  62. int position_;
  63. bool is_one_byte_;
  64. };

其实原理非常简单,用一个Vector容器去装字符,如果容量不够,会进行扩张。

暂时不管双字节字符(比如中文),所以需要关注的属性和方法就是上面的那些,有一个地方可以关注一下,就是扩容。根据扩容机制,初始会有16 * 4的容量,当所需容量大到一定程度,会写死,这里来计算一下写死的最大容量。

  1. /**
  2. * 计算 kMaxGrowth = 1024 * 1024 = 1048576
  3. * 得到阈值 (kMaxGrowth / (kGrowthFactor - 1) = 1048576 / (4 - 1) = 349525.333
  4. * 而未达到阈值前容器容量会从16开始每次乘以4 如下
  5. * 64 256 1024 4096 16384 65536 262144 1048576
  6. * 当扩容第7次时才出现比阈值大的数 这个值恰好等于1mb 因此容器容量最大值就是2mb
  7. */

单个字符串的解析长度原来是有上限的,最大为2mb,长度约为200万,此时会向Vector容量外的下标赋值,不知道会出现什么情况。

回到上一篇的结尾,由于匹配到单引号,所以会走ScanString方法,源码如下。

  1. Token::Value Scanner::ScanString() {
  2. uc32 quote = c0_;
  3. /**
  4. * 初始化
  5. */
  6. next().literal_chars.Start();
  7. while (true) {
  8. /**
  9. * 对字符串的结尾预检测
  10. */
  11. AdvanceUntil([this](uc32 c0) {
  12. // ...
  13. });
  14. /**
  15. * 遇到‘\’直接步进
  16. * 后面如果直接是字符串结尾标识符 判定为非法
  17. */
  18. while (c0_ == '\\') {
  19. Advance();
  20. if (V8_UNLIKELY(c0_ == kEndOfInput || !ScanEscape<false>())) {
  21. return Token::ILLEGAL;
  22. }
  23. }
  24. /**
  25. * 又遇到了同一个字符串标识符
  26. * 说明字符串解析完成
  27. */
  28. if (c0_ == quote) {
  29. Advance();
  30. return Token::STRING;
  31. }
  32. /**
  33. * 没有合拢的字符串 返回非法标记
  34. */
  35. if (V8_UNLIKELY(c0_ == kEndOfInput || unibrow::IsStringLiteralLineTerminator(c0_))) {
  36. return Token::ILLEGAL;
  37. }
  38. // 向Vector里面塞一个字符
  39. AddLiteralChar(c0_);
  40. }
  41. }

总的来说还是比较简单的,正常步进是初始化用过的Advance。代码中有一个方法叫AdvanceUntil,从函数名判断是一个预检函数。这个方法调用的结构非常奇怪,C++语法我也是TM日了狗,主要作用就是预先判断一下当前解析的字符串是否合法,整个函数结构如下。

  1. /**
  2. * 参数是一个匿名函数
  3. */
  4. AdvanceUntil([this](uc32 c0) {
  5. // Unicode大于127的特殊字符
  6. if (V8_UNLIKELY(static_cast<uint32_t>(c0) > kMaxAscii)) {
  7. /**
  8. * 检测是否是换行符
  9. * \r\n以及\n
  10. */
  11. if (V8_UNLIKELY(unibrow::IsStringLiteralLineTerminator(c0))) {
  12. return true;
  13. }
  14. AddLiteralChar(c0);
  15. return false;
  16. }
  17. /**
  18. * 检查是否是字符串结束符
  19. */
  20. uint8_t char_flags = character_scan_flags[c0];
  21. if (MayTerminateString(char_flags)) return true;
  22. AddLiteralChar(c0);
  23. return false;
  24. });
  25. /**
  26. * 这个方法会对c0_进行赋值
  27. */
  28. void AdvanceUntil(FunctionType check) {
  29. c0_ = source_->AdvanceUntil(check);
  30. }
  31. template <typename FunctionType>
  32. V8_INLINE uc32 AdvanceUntil(FunctionType check) {
  33. while (true) {
  34. /**
  35. * 从游标位置到结尾搜索符合条件的字符
  36. */
  37. auto next_cursor_pos =
  38. std::find_if(buffer_cursor_, buffer_end_, [&check](uint16_t raw_c0_) {
  39. uc32 c0_ = static_cast<uc32>(raw_c0_);
  40. return check(c0_);
  41. });
  42. /**
  43. * 1、碰到第二个参数 说明没有符合条件的字符 直接返回结束符
  44. * 2、有符合条件的字符 把游标属性指向该字符的后一位 返回该字符
  45. */
  46. if (next_cursor_pos == buffer_end_) {
  47. buffer_cursor_ = buffer_end_;
  48. if (!ReadBlockChecked()) {
  49. buffer_cursor_++;
  50. return kEndOfInput;
  51. }
  52. } else {
  53. buffer_cursor_ = next_cursor_pos + 1;
  54. return static_cast<uc32>(*next_cursor_pos);
  55. }
  56. }
  57. }

这里的调用方式比较邪门,其实就是JS的高阶函数,函数作为参数传入函数,比较核心的就是find_if方法与函数参数,这里就不讲std的方法了,用JS翻译一下,不然看起来实在太痛苦。

  1. const callback = (str) => IsStringLiteralLineTerminator(str);
  2. const AdvanceUntil = (callback) => {
  3. let tarArea = buffer_.slice(buffer_cursor_, buffer_end_);
  4. let tarIdx = tarArea.findIdx(v => callback(v));
  5. if(tarIdx === - 1) return '非法字符串';
  6. buffer_cursor_ = tarIdx + 1;
  7. c0_ = buffer_[tarIdx];
  8. }

就是这么简单,变量直接对应,逻辑的话也就上面这些,find_if也就是根据索引来找符合对应条件的值。也就是说,唯一需要讲解的就是字符串结束符的判断。

涉及的新属性有两个,其中一个是映射数组character_scan_flags,另外一个是MayTerminateString方法,两者其实是一个东西,可以放一起看。

  1. inline bool MayTerminateString(uint8_t scan_flags) {
  2. return (scan_flags & static_cast<uint8_t>(ScanFlags::kStringTerminator));
  3. }
  4. /**
  5. * 字符扫描标记
  6. */
  7. enum class ScanFlags : uint8_t {
  8. kTerminatesLiteral = 1 << 0,
  9. // "Cannot" rather than "can" so that this flag can be ORed together across
  10. // multiple characters.
  11. kCannotBeKeyword = 1 << 1,
  12. kCannotBeKeywordStart = 1 << 2,
  13. kStringTerminator = 1 << 3,
  14. kIdentifierNeedsSlowPath = 1 << 4,
  15. kMultilineCommentCharacterNeedsSlowPath = 1 << 5,
  16. };
  17. /**
  18. * 映射表
  19. * 对字符的可能性进行分类
  20. */
  21. static constexpr const uint8_t character_scan_flags[128] = {
  22. #define CALL_GET_SCAN_FLAGS(N) GetScanFlags(N),
  23. INT_0_TO_127_LIST(CALL_GET_SCAN_FLAGS)
  24. #undef CALL_GET_SCAN_FLAGS
  25. };

首先可以看出,character_scan_flags也是类似于之前那个Unicode与Ascii的表,对所有字符做一个映射,映射的值就是那个枚举类型,一个字符可能对应多个可能性。这里的计算方法可以参照我之前那篇利用枚举与位运算做配置,需要哪个属性,就用对应的枚举与字符映射值做与运算。

这个映射表的生成比较简单粗暴,会对每一个字符做6重或运算生成一个数,目前只看字符串终止符那块。

  1. constexpr uint8_t GetScanFlags(char c) {
  2. return
  3. /** 1 */ | /** 2 */ | /** 3 */ |
  4. // Possible string termination characters.
  5. ((c == '\'' || c == '"' || c == '\n' || c == '\r' || c == '\\')
  6. ? static_cast<uint8_t>(ScanFlags::kStringTerminator)
  7. : 0) | /** 5 */ | /** 6 */
  8. }

也就是说,当前字符是单双引号、换行与反斜杠时,会被认定可能是一个字符串的结尾。

回到编译字符串’Hello’,由于在字符结束之前,就存在另一个单引号,所以这个符号被认为可能是结束符号赋值给了c0_,Stream类的游标也直接移到了那个位置。至于中间的H、e、l、l、o5个字符,因为不存在任何特殊性,所以在最后的AddLiteralChar方法中被添加进了容器中。

结束后,整个函数正常返回Token::STRING作为词法结构体的类型,结构体的Literal_chars的容器则存储着对应的字符串。

转载于:https://www.cnblogs.com/QH-Jimmy/p/11134550.html

发表评论

表情:
评论列表 (有 0 条评论,453人围观)

还没有评论,来说两句吧...

相关阅读

    相关 V8引擎的内存收回

    V8引擎的内存收回 前言 JS语言不像C/C++, 让程序员自己去开辟或者释放内存,而是类似Java,采用自己的一套垃圾回收算法进行自动的内存管理。 我们知

    相关 谷歌v8引擎详解

    前言   JavaScript绝对是最火的编程语言之一,一直具有很大的用户群,随着在服务端的使用(NodeJs),更是爆发了极强的生命力。编程语言分为编译型语言和解释型语

    相关 深入V8引擎-AST(1)

      没办法了,开坑吧,接下来的几篇会讲述JavaScript字符串源码在v8中转换成AST(抽象语法树)的过程。   JS代码在V8的解析只有简单的几步,其中第一步就是将源字

    相关 深入V8引擎-AST(5)

    懒得发首页了,有时候因为贴的代码太多会被下,而且这东西本来也只是对自己学习的记录,阅读体验极差,所以就本地自娱自乐的写着吧! 由于是解析字符串,所以在开始之前介绍一下词法结构

    相关 v8引擎详解

    前言 `JavaScript`绝对是最火的编程语言之一,一直具有很大的用户群,随着在服务端的使用(`NodeJs`),更是爆发了极强的生命力。编程语言分为编译型语言和解释