Общие сведения о Chrome V8 – Глава 23. Анализ рабочего процесса компилятора, AST и токен
22 октября 2022 г.Добро пожаловать в другие главы Давайте разберемся с Chrome V8
В этой статье я расскажу о том, как Parse, AST и Token работают вместе, и проведу вас через компиляцию, а также посмотрю и увижу детали. В частности, вы увидите, как сканировать токены и как Parse использует токены для создания дерева AST.
1. Parse_Info
Ниже показан класс Parse_Info. Это класс ядра парсера, который управляет исходным кодом JavaScript и деревом AST. Проще говоря, вход парсера — это код JavaScript, а выход — дерево AST. Но обратите внимание, что с того момента, как V8 загрузил исходный код JavaScript, этот код уже был закодирован как UTF-16.
Перейдем к Parse_Info.
1. // A container for the inputs, configuration options, and outputs of parsing.
2. class V8_EXPORT_PRIVATE ParseInfo {
3. public:
4. //omit...
5. AstValueFactory* ast_value_factory() const {
6. DCHECK(ast_value_factory_.get());
7. return ast_value_factory_.get();
8. }
9. const AstRawString* function_name() const { return function_name_; }
10. void set_function_name(const AstRawString* function_name) {
11. function_name_ = function_name;
12. }
13. FunctionLiteral* literal() const { return literal_; }
14. void set_literal(FunctionLiteral* literal) { literal_ = literal; }
15. private:
16. //------------- Inputs to parsing and scope analysis -----------------------
17. const UnoptimizedCompileFlags flags_;
18. UnoptimizedCompileState* state_;
19. std::unique_ptr<Zone> zone_;
20. v8::Extension* extension_;
21. DeclarationScope* script_scope_;
22. uintptr_t stack_limit_;
23. int parameters_end_pos_;
24. int max_function_literal_id_;
25. //----------- Inputs+Outputs of parsing and scope analysis -----------------
26. std::unique_ptr<Utf16CharacterStream> character_stream_;
27. std::unique_ptr<ConsumedPreparseData> consumed_preparse_data_;
28. std::unique_ptr<AstValueFactory> ast_value_factory_;
29. const AstRawString* function_name_;
30. RuntimeCallStats* runtime_call_stats_;
31. SourceRangeMap* source_range_map_; // Used when block coverage is enabled.
32. //----------- Output of parsing and scope analysis ------------------------
33. FunctionLiteral* literal_;
34. bool allow_eval_cache_ : 1;
35. #if V8_ENABLE_WEBASSEMBLY
36. bool contains_asm_module_ : 1;
37. #endif // V8_ENABLE_WEBASSEMBLY
38. LanguageMode language_mode_ : 1;
39. };
В приведенном выше коде строка 5 — это фабричный метод AST; Строка 13 литерал возвращает дерево AST; Строка 17 flags_ показывает причину, по которой функция не оптимизирована; Строка 33, literal_, отвечает за хранение завершенного дерева AST. Как я уже упоминал, AST — это выходные данные синтаксического анализатора, поэтому, если вы хотите подробно наблюдать за рабочим процессом синтаксического анализатора, вы просто отлаживаете его и продолжаете наблюдать за переменной-членом literal_, таким образом вы увидите генерацию AST и как вызывается фабричный метод.
2. Дерево AST
Как я упоминал выше, переменная literal_ представляет дерево AST, классом объявления которого является FunctionLiteral.
1. class FunctionLiteral final : public Expression {
2. public:
3. template <typename IsolateT>//省略很多代码...............
4. MaybeHandle<String> GetName(IsolateT* isolate) const { }
5. const AstConsString* raw_name() const { return raw_name_; }
6. DeclarationScope* scope() const { return scope_; }
7. ZonePtrList<Statement>* body() { return &body_; }
8. bool is_anonymous_expression() const {
9. return syntax_kind() == FunctionSyntaxKind::kAnonymousExpression;
10. }
11. V8_EXPORT_PRIVATE LanguageMode language_mode() const;
12. void add_expected_properties(int number_properties) {}
13. std::unique_ptr<char[]> GetDebugName() const;
14. Handle<String> GetInferredName(Isolate* isolate) { }
15. Handle<String> GetInferredName(LocalIsolate* isolate) const {}
16. const AstConsString* raw_inferred_name() { return raw_inferred_name_; }
17. FunctionSyntaxKind syntax_kind() const {}
18. FunctionKind kind() const;
19. bool IsAnonymousFunctionDefinition() const { }
20. int suspend_count() { return suspend_count_; }
21. int function_literal_id() const { return function_literal_id_; }
22. void set_function_literal_id(int function_literal_id) { }
23. private:
24. const AstConsString* raw_name_;
25. DeclarationScope* scope_;
26. ZonePtrList<Statement> body_;
27. AstConsString* raw_inferred_name_;
28. Handle<String> inferred_name_;
29. ProducedPreparseData* produced_preparse_data_;
30. };
Подчеркну, что гранулярность AST — это одна функция, другими словами, функция JavaScript соответствует только одному дереву AST. В приведенном выше коде переменная-член body_ в строке 26 содержит тело AST, просмотрите body_, если вас интересуют подробности. Строки 4–15 задают языковой режим как строгий или неаккуратный.
Рассмотрим подробнее строку 18 FunctionKind.
1. enum FunctionKind : uint8_t {
2. // BEGIN constructable functions
3. kNormalFunction,kModule,kAsyncModule,kBaseConstructor,kDefaultBaseConstructor,
4. kDefaultDerivedConstructor,
5. kDerivedConstructor,
6. //omit....
7. kLastFunctionKind = kClassStaticInitializerFunction,
8. };
Вы должны заметить, что типы функций не являются типами в книгах по JavaScript, потому что типы функций просто представляют типы функций в компиляторе. Подробнее см. в книгах по компиляторам.
3. Тип узла AST
Следующие шаблоны макросов не являются исчерпывающими списками типов узлов.
1. #define DECLARATION_NODE_LIST(V)
2. V(VariableDeclaration)
3. V(FunctionDeclaration)
4. #define ITERATION_NODE_LIST(V)
5. V(DoWhileStatement)
6. V(WhileStatement)
7. V(ForStatement)
8. V(ForInStatement)
9. V(ForOfStatement)
10. #define BREAKABLE_NODE_LIST(V)
11. V(Block)
12. V(SwitchStatement)
13. #define STATEMENT_NODE_LIST(V)
14. ITERATION_NODE_LIST(V)
15. BREAKABLE_NODE_LIST(V)
16. V(ExpressionStatement)
17. V(EmptyStatement)
18. V(SloppyBlockFunctionStatement)
19. V(IfStatement)
20. V(InitializeClassStaticElementsStatement)//omit
21. #define LITERAL_NODE_LIST(V)
22. V(RegExpLiteral)
23. V(ObjectLiteral)
24. V(ArrayLiteral)
25. #define EXPRESSION_NODE_LIST(V)
26. LITERAL_NODE_LIST(V)
27. V(Assignment)
28. V(Await)
29. V(BinaryOperation)
30. V(NaryOperation)
31. V(Call)
32. V(YieldStar)//omit
33. #define FAILURE_NODE_LIST(V) V(FailureExpression)
34. #define AST_NODE_LIST(V)
35. DECLARATION_NODE_LIST(V)
36. STATEMENT_NODE_LIST(V)
37. EXPRESSION_NODE_LIST(V)
В приведенном выше коде (см. строку 34) все шаблоны объединяются вместе для создания дерева AST для вашей функции JavaScript в синтаксическом анализаторе, который является частью конвейера компилятора. По макросу AST_NODE_LIST мы должны увидеть, что в AST есть три типа, и каждый тип также имеет больше подтипов. Все эти типы соответствуют нашему коду JavaScript и точно представляют семантику.
4. Тип токена
Следующие шаблоны макросов не являются исчерпывающими списками токенов.
1. #define TOKEN_LIST(T, K)
2. T(TEMPLATE_SPAN, nullptr, 0)
3. T(TEMPLATE_TAIL, nullptr, 0)
4. /* BEGIN Property */
5. T(PERIOD, ".", 0)
6. T(LBRACK, "[", 0)
7. /* END Property */
8. /* END Member */
9. T(QUESTION_PERIOD, "?.", 0)
10. T(LPAREN, "(", 0)
11. /* END PropertyOrCall */
12. T(RPAREN, ")", 0)
13. T(RBRACK, "]", 0)
14. T(LBRACE, "{", 0)
15. T(COLON, ":", 0)
16. T(ELLIPSIS, "...", 0)
17. T(CONDITIONAL, "?", 3)
18. //omit.
Токен используется в сканере, который является частью компилятора V8. Сканер отвечает за преобразование кода JavaScript в токены. Скажем, простой пример, см. строку 5, тип токена — PERIOD и соответствует точке в коде JavaScript, что означает, что сканер будет преобразовывать каждую точку, которую он видит, в токен PERIOD. Точно так же строки 10 и 12 представляют собой легкие и правые скобки, которые используются для описания (
и )
в коде JavaScript. Итак, в каждой строке, в каждом макросе, который начинается с буквы T, первый параметр — это тип токена, второй — лексический, соответствующий типу, а последнее число — это приоритет токена, число меньше, токен выше, и будет преобразован раньше.
5. Автоматизация с конечными состояниями
В большинстве компиляторов лексер и синтаксический анализатор используют FSA (автоматизацию с конечным состоянием) для анализа исходного кода. В V8 для реализации FSA используется switch-case, давайте взглянем на лексер FSA, а именно на сканер токенов.
1. V8_INLINE Token::Value Scanner::ScanSingleToken() {
2. Token::Value token;
3. do {
4. next().location.beg_pos = source_pos();
5. if (V8_LIKELY(static_cast<unsigned>(c0_) <= kMaxAscii)) {
6. token = one_char_tokens[c0_];
7. switch (token) {
8. case Token::LPAREN:
9. case Token::RPAREN:
10. case Token::LBRACE:
11. case Token::RBRACE:
12. case Token::LBRACK:
13. case Token::RBRACK:
14. case Token::COLON:
15. case Token::SEMICOLON:
16. case Token::COMMA:
17. case Token::BIT_NOT:
18. case Token::ILLEGAL:
19. // One character tokens.
20. return Select(token);
21. case Token::CONDITIONAL:
22. // ? ?. ?? ??=
23. Advance();
24. if (c0_ == '.') {
25. Advance();
26. if (!IsDecimalDigit(c0_)) return Token::QUESTION_PERIOD;
27. PushBack('.');
28. } else if (c0_ == '?') {
29. return Select('=', Token::ASSIGN_NULLISH, Token::NULLISH);
30. }
31. return Token::CONDITIONAL;
32. //omit
33. case Token::WHITESPACE:
34. token = SkipWhiteSpace();
35. continue;
36. case Token::NUMBER:
37. return ScanNumber(false);
38. case Token::IDENTIFIER:
39. return ScanIdentifierOrKeyword();
40. default:
41. UNREACHABLE();
42. }
43. }
44. if (IsIdentifierStart(c0_) ||
45. (CombineSurrogatePair() && IsIdentifierStart(c0_))) {
46. return ScanIdentifierOrKeyword();
47. }
48. } while (token == Token::WHITESPACE);
49. return token;
50. }
ScanSingleToken() отвечает за создание токена. В строках 8–40 каждый случай является токеном.
Следующий код представляет собой синтаксический анализатор FSA, который использует токены и создает узлы для дерева AST.
1. ParserBase<Impl>::ParseStatementListItem() {
2. switch (peek()) {
3. case Token::FUNCTION:
4. return ParseHoistableDeclaration(nullptr, false);
5. case Token::CLASS:
6. Consume(Token::CLASS);
7. return ParseClassDeclaration(nullptr, false);
8. case Token::VAR:
9. case Token::CONST:
10. return ParseVariableStatement(kStatementListItem, nullptr);
11. case Token::LET:
12. if (IsNextLetKeyword()) {
13. return ParseVariableStatement(kStatementListItem, nullptr);
14. }
15. break;
16. case Token::ASYNC:
17. if (PeekAhead() == Token::FUNCTION &&
18. !scanner()->HasLineTerminatorAfterNext()) {
19. Consume(Token::ASYNC);
20. return ParseAsyncFunctionDeclaration(nullptr, false);
21. }
22. break;
23. default:
24. break;
25. }
26. return ParseStatement(nullptr, nullptr, kAllowLabelledFunctionStatement);
27. }
В конце концов, я бы сказал, что FSA — это важная основа компилятора, которую стоит изучить немного подробнее.
Хорошо, на этом мы заканчиваем. Увидимся в следующий раз, будьте осторожны!
Пожалуйста, свяжитесь со мной, если у вас есть какие-либо проблемы. WeChat: qq9123013 Электронная почта: v8blink@outlook.com
:::подсказка Эта история была впервые опубликована здесь. .
:::
Оригинал