Общие сведения о Chrome V8 – Глава 23. Анализ рабочего процесса компилятора, AST и токен

Общие сведения о 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

:::подсказка Эта история была впервые опубликована здесь. .

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE