Давайте разберемся в Chrome V8 — Глава 3: Конвейер компиляции, сканер

Первоисточник: https://medium.com/@huidou/lets-understand-chrome-v8-d34028b37e03

Добро пожаловать в другие главы «Давайте поймем Chrome V8».

1. Конвейер компиляции

Когда JS-компилятор инициализируется, вызывается Scanner для генерации токена и сохранения его в кэше. Затем начинает работать парсер, который берет токен из кэша и генерирует узел дерева AST. Когда парсер сталкивается с ошибкой в кэше, он вызывает сканер для генерации маркера и сохранения его в кэше, а затем продолжает работу. И так далее, пока AST не будет сгенерирован полностью. Как показано на рисунке 1.

Давайте посмотрим на следующий код, Parser::ParseProgram является входной функцией Scanner, которая указана на рисунке 1. Далее рассмотрим scanner_.Initialize() и FunctionLiteral* result = DoParseProgram(isolate, info), их роль такова:

1. Инициализация сканера: генерируем токен и делаем так, чтобы scanner.c0_ указывал на первый символ js-кода, который будет скомпилирован.

2. DoParserProgram: извлечь токен для генерации AST-узла, затем вставить узел в AST-дерево.

void Parser::ParseProgram(Isolate* isolate, Handle<Script> script,
                          ParseInfo* info,
                          MaybeHandle<ScopeInfo> maybe_outer_scope_info) {
  DCHECK_EQ(script->id(), flags().script_id());

  // It's OK to use the Isolate & counters here, since this function is only
  // called in the main thread.
  DCHECK(parsing_on_main_thread_);
  RCS_SCOPE(runtime_call_stats_, flags().is_eval()
                                     ? RuntimeCallCounterId::kParseEval
                                     : RuntimeCallCounterId::kParseProgram);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"), "V8.ParseProgram");
  base::ElapsedTimer timer;
  if (V8_UNLIKELY(FLAG_log_function_events)) timer.Start();

  // Initialize parser state.
  DeserializeScopeChain(isolate, info, maybe_outer_scope_info,
                        Scope::DeserializationMode::kIncludingVariables);

  DCHECK_EQ(script->is_wrapped(), info->is_wrapped_as_function());
  if (script->is_wrapped()) {
    maybe_wrapped_arguments_ = handle(script->wrapped_arguments(), isolate);
  }

  scanner_.Initialize();
  FunctionLiteral* result = DoParseProgram(isolate, info);
  MaybeResetCharacterStream(info, result);
  MaybeProcessSourceRanges(info, result, stack_limit_);
  PostProcessParseResult(isolate, info, result);

  HandleSourceURLComments(isolate, script);

  if (V8_UNLIKELY(FLAG_log_function_events) && result != nullptr) {
    double ms = timer.Elapsed().InMillisecondsF();
    const char* event_name = "parse-eval";
    int start = -1;
    int end = -1;
    if (!flags().is_eval()) {
      event_name = "parse-script";
      start = 0;
      end = String::cast(script->source()).length();
    }
    LOG(isolate,
        FunctionEvent(event_name, flags().script_id(), ms, start, end, "", 0));
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Parser::ParseProgram — это самый близкий способ отладки исходного кода JS компилятора, после глубокой отладки вы увидите полный рабочий процесс компилятора. Кстати, программа Parser::ParseProgram окружена множеством внешних вызовов, эти вызовы просто делают некоторые тривиальные, но неважные вещи, поэтому, чтобы увидеть рабочий процесс компиляции, достаточно программы Parser::ParseProgram, отлаживайте ее!

Вам также необходимо знать: Прежде чем вы перейдете в Parser::ParseProgram, Scanner сгенерировал для вас токен, в инициализации, упомянутой выше.

2. Рабочий процесс Scanner

Лексический анализ — это первая часть работы компилятора. В V8 функция Scanner отвечает за реализацию лексического анализа. Сканер читает исходный поток JS и генерирует лексемы.

Поток JS кодируется в UTF-16, а исходный код выглядит следующим образом:

// ---------------------------------------------------------------------
// Buffered stream of UTF-16 code units, using an internal UTF-16 buffer.
// A code unit is a 16 bit value representing either a 16 bit code point
// or one part of a surrogate pair that make a single 21 bit code point.
class Utf16CharacterStream {
 public:
  static constexpr base::uc32 kEndOfInput = static_cast<base::uc32>(-1);

  virtual ~Utf16CharacterStream() = default;

  V8_INLINE void set_parser_error() {
    buffer_cursor_ = buffer_end_;
    has_parser_error_ = true;
  }
  V8_INLINE void reset_parser_error_flag() { has_parser_error_ = false; }
  V8_INLINE bool has_parser_error() const { return has_parser_error_; }

  inline base::uc32 Peek() {
    if (V8_LIKELY(buffer_cursor_ < buffer_end_)) {
      return static_cast<base::uc32>(*buffer_cursor_);
    } else if (ReadBlockChecked()) {
      return static_cast<base::uc32>(*buffer_cursor_);
    } else {
      return kEndOfInput;
    }
  }
  // Returns and advances past the next UTF-16 code unit in the input
  // stream. If there are no more code units it returns kEndOfInput.
  inline base::uc32 Advance() {
    base::uc32 result = Peek();
    buffer_cursor_++;
    return result;
  }

//...............
//omit.............
//..............
  // Read more data, and update buffer_*_ to point to it.
  // Returns true if more data was available.
  //
  // ReadBlock() may modify any of the buffer_*_ members, but must sure that
  // the result of pos() remains unaffected.
  //
  // Examples:
  // - a stream could either fill a separate buffer. Then buffer_start_ and
  //   buffer_cursor_ would point to the beginning of the buffer, and
  //   buffer_pos would be the old pos().
  // - a stream with existing buffer chunks would set buffer_start_ and
  //   buffer_end_ to cover the full chunk, and then buffer_cursor_ would
  //   point into the middle of the buffer, while buffer_pos_ would describe
  //   the start of the buffer.
  virtual bool ReadBlock() = 0;

  const uint16_t* buffer_start_;
  const uint16_t* buffer_cursor_;
  const uint16_t* buffer_end_;
  size_t buffer_pos_;
  RuntimeCallStats* runtime_call_stats_;
  bool has_parser_error_ = false;
};
Войти в полноэкранный режим Выход из полноэкранного режима

В приведенном выше коде есть несколько важных членов: buffer_start_, buffer_cursor_, buffer_end_, их роли смотрите в комментариях. На рисунке 2 показан пример Utf16CharacterStream.

Левая часть рисунка 2 — это исходный код JS, а правая — соответствующий строковый поток.

1.  bool ReadBlock() final {
2.    size_t position = pos();
3.    buffer_pos_ = position;
4.    buffer_start_ = &buffer_[0];
5.    buffer_cursor_ = buffer_start_;
6.    DisallowGarbageCollection no_gc;
7.    Range<uint8_t> range =
8.        byte_stream_.GetDataAt(position, runtime_call_stats(), &no_gc);
9.    if (range.length() == 0) {
10.      buffer_end_ = buffer_start_;
11.      return false;
12.    }
13.    size_t length = std::min({kBufferSize, range.length()});
14.    i::CopyChars(buffer_, range.start, length);
15.    buffer_end_ = &buffer_[length];
16.    return true;
17.  }
Вход в полноэкранный режим Выход из полноэкранного режима

В строке 3 приведенного выше кода buffer_start_ указывает на первый символ потока JS. Сканер считывает каждый символ JS-потока, перемещая указатель buffer_start_.

На рисунке 3 показана еще одна важная функция — Advance(), которая считывает каждый символ по буферу_start_++.

Вышеупомянутый scanner.c0_, он всегда указывает на первый символ в строке, используемой для генерации токена. Следующий код показывает роль c0_.

void Advance() {
    if (capture_raw) {
      AddRawLiteralChar(c0_);
    }
    c0_ = source_->Advance();//Here!!
  }
Вход в полноэкранный режим Выход из полноэкранного режима

В Scanner мы видим следующий метод, который используется для генерации токена.

1.  void Scanner::Scan(TokenDesc* next_desc) {
2.    DCHECK_EQ(next_desc, &next());
3.    next_desc->token = ScanSingleToken();
4.    DCHECK_IMPLIES(has_parser_error(), next_desc->token == Token::ILLEGAL);
5.    next_desc->location.end_pos = source_pos();
6.  #ifdef DEBUG
7.    SanityCheckTokenDesc(current());
8.    SanityCheckTokenDesc(next());
9.    SanityCheckTokenDesc(next_next());
10.  #endif
11.  }
Войти в полноэкранный режим Выход из полноэкранного режима

Строка 23 в приведенном выше коде говорит нам, что next_desc — это структура данных, описывающая токен, и она показана ниже:

struct TokenDesc {
    Location location = {0, 0};
    LiteralBuffer literal_chars;
    LiteralBuffer raw_literal_chars;
    Token::Value token = Token::UNINITIALIZED;
    MessageTemplate invalid_template_escape_message = MessageTemplate::kNone;
    Location invalid_template_escape_location;
    uint32_t smi_value_ = 0;
    bool after_line_terminator = false;

#ifdef DEBUG
    bool CanAccessLiteral() const {
      return token == Token::PRIVATE_NAME || token == Token::ILLEGAL ||
             token == Token::ESCAPED_KEYWORD || token == Token::UNINITIALIZED ||
             token == Token::REGEXP_LITERAL ||
             base::IsInRange(token, Token::NUMBER, Token::STRING) ||
             Token::IsAnyIdentifier(token) || Token::IsKeyword(token) ||
             base::IsInRange(token, Token::TEMPLATE_SPAN, Token::TEMPLATE_TAIL);
    }
    bool CanAccessRawLiteral() const {
      return token == Token::ILLEGAL || token == Token::UNINITIALIZED ||
             base::IsInRange(token, Token::TEMPLATE_SPAN, Token::TEMPLATE_TAIL);
    }
#endif  // DEBUG
  };
Вход в полноэкранный режим Выход из полноэкранного режима

На данный момент инициализация сканера завершена, и затем парсер вызывает сканер для генерации маркера. Scan() — это вход сканера, он показан ниже.

void Scanner::Scan(TokenDesc* next_desc) {
  DCHECK_EQ(next_desc, &next());

  next_desc->token = ScanSingleToken();
  DCHECK_IMPLIES(has_parser_error(), next_desc->token == Token::ILLEGAL);
  next_desc->location.end_pos = source_pos();

#ifdef DEBUG
  SanityCheckTokenDesc(current());
  SanityCheckTokenDesc(next());
  SanityCheckTokenDesc(next_next());
#endif
}
Вход в полноэкранный режим Выход из полноэкранного режима

3. Генерация токена


На рисунке 4 getNextToken берет токен из кэша и отправляет его в Parser, который генерирует AST-узел.

1.  void ParserBase<Impl>::ParseStatementList(StatementListT* body,
2.                                            Token::Value end_token) {
3.    // StatementList ::
4.    //   (StatementListItem)* <end_token>
5.    DCHECK_NOT_NULL(body);
6.    while (peek() == Token::STRING) {
7.      bool use_strict = false;
8.  #if V8_ENABLE_WEBASSEMBLY
9.      bool use_asm = false;
10.  #endif  // V8_ENABLE_WEBASSEMBLY
11.      Scanner::Location token_loc = scanner()->peek_location();
12.      if (scanner()->NextLiteralExactlyEquals("use strict")) {
13.        use_strict = true;
14.  #if V8_ENABLE_WEBASSEMBLY
15.      } else if (scanner()->NextLiteralExactlyEquals("use asm")) {
16.        use_asm = true;
17.  #endif  // V8_ENABLE_WEBASSEMBLY
18.      }
19.      StatementT stat = ParseStatementListItem();
20.  //.......................
21.  //omit
22.  //.......................
23.    }
24.    while (peek() != end_token) {
25.      StatementT stat = ParseStatementListItem();
26.      if (impl()->IsNull(stat)) return;
27.      if (stat->IsEmptyStatement()) continue;
28.      body->Add(stat);
29.    }
30.  }
31.  }
Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше коде, ограниченном тестовым примером, наша отладка не может охватить весь код. 25-я строка кода очень важна, она отвечает за разбор каждого оператора JS, на рисунке 5 показан стек вызовов функций.


На рисунке 5 также виден вышеупомянутый кэш, который реализуется функцией Consum(). Код генерации токенов приведен ниже.

V8_INLINE Token::Value Scanner::ScanSingleToken() {
  Token::Value token;
  do {
    next().location.beg_pos = source_pos();

    if (V8_LIKELY(static_cast<unsigned>(c0_) <= kMaxAscii)) {
      token = one_char_tokens[c0_];

      switch (token) {
//..............
//omit
//..............

        case Token::CONDITIONAL:
          // ? ?. ?? ??=
          Advance();
          if (c0_ == '.') {
            Advance();
            if (!IsDecimalDigit(c0_)) return Token::QUESTION_PERIOD;
            PushBack('.');
          } else if (c0_ == '?') {
            return Select('=', Token::ASSIGN_NULLISH, Token::NULLISH);
          }
          return Token::CONDITIONAL;
        case Token::STRING:
          return ScanString();
        case Token::LT:
          // < <= << <<= <!--
          Advance();
          if (c0_ == '=') return Select(Token::LTE);
          if (c0_ == '<') return Select('=', Token::ASSIGN_SHL, Token::SHL);
          if (c0_ == '!') {
            token = ScanHtmlComment();
            continue;
          }
          return Token::LT;
        case Token::ASSIGN:
          // = == === =>
          Advance();
          if (c0_ == '=') return Select('=', Token::EQ_STRICT, Token::EQ);
          if (c0_ == '>') return Select(Token::ARROW);
          return Token::ASSIGN;

        case Token::NOT:
          // ! != !==
          Advance();
          if (c0_ == '=') return Select('=', Token::NE_STRICT, Token::NE);
          return Token::NOT;
        case Token::ADD:
          // + ++ +=
          Advance();
          if (c0_ == '+') return Select(Token::INC);
          if (c0_ == '=') return Select(Token::ASSIGN_ADD);
          return Token::ADD;
//..............
//omit
//..............
        default:
          UNREACHABLE();
      }
    }
    if (IsIdentifierStart(c0_) ||
        (CombineSurrogatePair() && IsIdentifierStart(c0_))) {
      return ScanIdentifierOrKeyword();
    }
    if (c0_ == kEndOfInput) {
      return source_->has_parser_error() ? Token::ILLEGAL : Token::EOS;
    }
    token = SkipWhiteSpace();
    // Continue scanning for tokens as long as we're just skipping whitespace.
  } while (token == Token::WHITESPACE);

  return token;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Видно, что процесс генерации маркеров представляет собой switch case, который является конечным автоматом состояний. В процессе генерации токенов также используется множество регулярных выражений.

На самом деле, в V8 существует множество шаблонов токенов (TOKEN_LIST), и V8 использует эти шаблоны для подбора символов для генерации токенов.

Ладно, на этом мы заканчиваем эту часть. Увидимся в следующий раз, ребята, всего доброго!

Мой блог — cncyclops.com. Пожалуйста, свяжитесь со мной, если у вас возникнут какие-либо вопросы.

WeChat: qq9123013 Email: v8blink@outlook.com

Оцените статью
devanswers.ru
Добавить комментарий