美文网首页
V8词法分析(Scanner)

V8词法分析(Scanner)

作者: JunhYan | 来源:发表于2021-01-15 17:03 被阅读0次

概述

对于js源码,需要经过一系列的操作才能让V8认识它们。在之前的概率里也有介绍,V8最初会把js源码解析成一个AST(abstract syntax tree虚拟语法树),然后把AST编译成字节码。在这一系列的操作中,第一个步骤就是对js源码做词法分析,在V8中,就是scanner要干的事情。
输入的js源码,从Blink给到V8的时候可能是各种形式的编码方式,比如ASCLL、Latin1、UTF-8或UTF-16,但V8对字符流的解析只能是统一的一种。V8引擎会把接收到的源码当做字符串存储在V8的堆内存中,然后使用ScannerStream::For方法,把字符串转换Utf16CharacterStream字符流,V8之所以选择UTF-16的原因就是这个编码方式是向下兼容ASCLL、Latin1、UTF-8的。

JS源码入口

对于js源码的解析和编译是在主线程中完成的,整个函数调用栈,如果有兴趣,可以通过V8的源码跟踪一下从接收js源码的ScriptCompiler::Compile函数到下面CompileScriptOnMainThread的调用过程。里面涉及到了很多的细节操作,比如script_details的初始化,初始options的设置等。与CompileScriptOnMainThread相对应的有CompileOnBackgroundThread,从函数名字大家应该也可以猜到一个是主线程编译,一个是后台线程编译。具体怎么后台编译在这里就不做跟多介绍了。

MaybeHandle<SharedFunctionInfo> CompileScriptOnMainThread(
    const UnoptimizedCompileFlags flags, Handle<String> source,
    const Compiler::ScriptDetails& script_details,
    ScriptOriginOptions origin_options, NativesFlag natives,
    v8::Extension* extension, Isolate* isolate,
    IsCompiledScope* is_compiled_scope) {
  UnoptimizedCompileState compile_state(isolate);
  ParseInfo parse_info(isolate, flags, &compile_state);
  parse_info.set_extension(extension);

  Handle<Script> script = NewScript(isolate, &parse_info, source,
                                    script_details, origin_options, natives);
  DCHECK_IMPLIES(parse_info.flags().collect_type_profile(),
                 script->IsUserJavaScript());
  DCHECK_EQ(parse_info.flags().is_repl_mode(), script->is_repl_mode());

  return CompileToplevel(&parse_info, script, isolate, is_compiled_scope);
}

在上面的代码中中流程性的函数是CompileToplevel,top level对应是lazy parse,对于js的源码,有些代码是要立刻去解析并且编译的,但有些代码是可以滞后解析的,具体lazy parse这部分在后面的章节里面做进一步的介绍。那我们看下CompileToplevel在正常的编译情况下会做的事情

 if (parse_info->literal() == nullptr &&
      !parsing::ParseProgram(parse_info, script, maybe_outer_scope_info,
                             isolate, parsing::ReportStatisticsMode::kYes)) {
    FailWithPendingException(isolate, script, parse_info,
                             Compiler::ClearExceptionFlag::KEEP_EXCEPTION);
    return MaybeHandle<SharedFunctionInfo>();
  }

如上代码,CompileToplevel中一个重要的工作就是调用parsing::ParseProgram方法,parse_info是对应的js源码的解析信息实例。
当然这个是我们确认我们要解析的是top level的js源码,所以使用的是ParseProgram方法,还有ParseFunction方法,与ParseProgram类似,是对于方法独立的操作,并且已经分配了SharedFunctionInfo(laze parse)。SharedFunctionInfo是对于需要懒解析的js方法的的信息的描述。如果不确定是不是top level,会使用ParseAny方法来做辨认后再调用ParseProgram或者ParseFunction。

下面看看parsing::ParseProgram的具体实现

bool ParseProgram(ParseInfo* info, Handle<Script> script,
                  MaybeHandle<ScopeInfo> maybe_outer_scope_info,
                  Isolate* isolate, ReportStatisticsMode mode) {
  DCHECK(info->flags().is_toplevel());
  DCHECK_NULL(info->literal());

  VMState<PARSER> state(isolate);

  // Create a character stream for the parser.
  Handle<String> source(String::cast(script->source()), isolate);
  isolate->counters()->total_parse_size()->Increment(source->length());
  std::unique_ptr<Utf16CharacterStream> stream(
      ScannerStream::For(isolate, source));
  info->set_character_stream(std::move(stream));

  Parser parser(info);

  // Ok to use Isolate here; this function is only called in the main thread.
  DCHECK(parser.parsing_on_main_thread_);
  parser.ParseProgram(isolate, script, info, maybe_outer_scope_info);
  MaybeReportErrorsAndStatistics(info, script, isolate, &parser, mode);
  return info->literal() != nullptr;
}

这个函数干了三件重要的事情
1、在上面提到过的通过ScannerStream::For(isolate, source)统一了输入的字符流为Utf16CharacterStream
2、
通过info->set_character_stream更新了parse info的character_stream_值,这个是本章的主角scanner的真正的入参。

void ParseInfo::set_character_stream(
    std::unique_ptr<Utf16CharacterStream> character_stream) {
  DCHECK_NULL(character_stream_);
  character_stream_.swap(character_stream);
}

3、创建一个parser,并调用parser的ParseProgram函数。

Parser简介

先简单的认识下parser,从他的构造函数开始

Parser::Parser(ParseInfo* info)
    : ParserBase<Parser>(
          info->zone(), &scanner_, info->stack_limit(), info->extension(),
          info->GetOrCreateAstValueFactory(), info->pending_error_handler(),
          info->runtime_call_stats(), info->logger(), info->flags(), true),
      info_(info),
      scanner_(info->character_stream(), flags()),
      preparser_zone_(info->zone()->allocator(), ZONE_NAME),
      reusable_preparser_(nullptr),
      mode_(PARSE_EAGERLY),  // Lazy mode must be set explicitly.
      source_range_map_(info->source_range_map()),
      total_preparse_skipped_(0),
      consumed_preparse_data_(info->consumed_preparse_data()),
      preparse_data_buffer_(),
      parameters_end_pos_(info->parameters_end_pos())

这里就只展示了构造函数的一些初始化属性,从这里其实就可以看到一个parser包含哪些属性比如包含ParseInfo、Scanner、Mode、PreParser等。从上面我们可以看到,parser对应的scanner的入参是info->character_stream()就是我们上面提到的character_stream_值,mode_默认为PARSE_EAGERLY,如果是要做lazy parse,在初始化的时候一定会标明为PARSE_LAZILY。

Scanner入口及实现

入口

然后看下parser的ParseProgram具体做了哪些事情

void Parser::ParseProgram(Isolate* isolate, Handle<Script> script,
                          ParseInfo* info,
                          MaybeHandle<ScopeInfo> maybe_outer_scope_info) {
  ...
  scanner_.Initialize();
  FunctionLiteral* result = DoParseProgram(isolate, info);
  ...
}

这个函数关键的两个功能就是上面没有省去的代码,第一个是初始化一个Scanner,第二个是调用DoParseProgram真正的开始解析js源码。先看一下Scanner的Initialize都做了那些事情

void Scanner::Initialize() {
  // Need to capture identifiers in order to recognize "get" and "set"
  // in object literals.
  Init();
  next().after_line_terminator = true;
  Scan();
}

在初始化的时候,看到上面的代码调用了Scan,Scan函数是用来做词法分析的函数,还没有真正的去解析js源码,为什么也调用了词法分析的函数呢?原因在于在真正解析js源码前,去判断下第一个Token是不是String类型的,如果是String类型的,那就有可能是'use strict'或者'use asm',这个对之后的解析会起到一定的约束作用。
在DoParseProgram中,会根据一个Token的情况继续往下解析,具体实现一般都会调用Consume函数记录当前的Token,并且使用Next函数调用Scan来继续解析之后的字符。

Scan扫描操作

具体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();
}
void Scanner::Scan() { Scan(next_); }

这里涉及到了C++的方法重构,这个在js里面是没有的,Scan默认从next_指针开始。这这里介绍Scanner里面的几个比较重要的参数

  TokenDesc* current_;    // desc for current token (as returned by Next())
  TokenDesc* next_;       // desc for next token (one token look-ahead)
  TokenDesc* next_next_;  // desc for the token after next (after PeakAhead())

  // Input stream. Must be initialized to an Utf16CharacterStream.
  Utf16CharacterStream* const source_;

  // One Unicode character look-ahead; c0_ < 0 at the end of the input.
  uc32 c0_;

TokenDesc结构体是对当前Token的描述,包括了当前Token的Location(起始位置),Token的Value,初始化是Token::UNINITIALIZED。current_是当前的Token,Next函数返回的是当前的Token描述,next_是即将需要分析的Token描述,next_next_是在next_后的Token描述,有点绕,尤其是Next函数的返回。source_是Sanner接收的字符流,c0_是比较关键的一个字段,是即将被解析的字符。在Scan中一个最关键的操作就是ScanSingleToken(),通过该函数来做真正的词法分析。这个函数是整个Scanner中的一个很重要的操作,所以在这里把这个函数整个的做一个较为详细的介绍

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) {
        case Token::LPAREN:
        case Token::RPAREN:
        case Token::LBRACE:
        case Token::RBRACE:
        case Token::LBRACK:
        case Token::RBRACK:
        case Token::COLON:
        case Token::SEMICOLON:
        case Token::COMMA:
        case Token::BIT_NOT:
        case Token::ILLEGAL:
          // One character tokens.
          return Select(token);

        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::GT:
          // > >= >> >>= >>> >>>=
          Advance();
          if (c0_ == '=') return Select(Token::GTE);
          if (c0_ == '>') {
            // >> >>= >>> >>>=
            Advance();
            if (c0_ == '=') return Select(Token::ASSIGN_SAR);
            if (c0_ == '>') return Select('=', Token::ASSIGN_SHR, Token::SHR);
            return Token::SAR;
          }
          return Token::GT;

        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;

        case Token::SUB:
          // - -- --> -=
          Advance();
          if (c0_ == '-') {
            Advance();
            if (c0_ == '>' && next().after_line_terminator) {
              // For compatibility with SpiderMonkey, we skip lines that
              // start with an HTML comment end '-->'.
              token = SkipSingleHTMLComment();
              continue;
            }
            return Token::DEC;
          }
          if (c0_ == '=') return Select(Token::ASSIGN_SUB);
          return Token::SUB;

        case Token::MUL:
          // * *=
          Advance();
          if (c0_ == '*') return Select('=', Token::ASSIGN_EXP, Token::EXP);
          if (c0_ == '=') return Select(Token::ASSIGN_MUL);
          return Token::MUL;

        case Token::MOD:
          // % %=
          return Select('=', Token::ASSIGN_MOD, Token::MOD);

        case Token::DIV:
          // /  // /* /=
          Advance();
          if (c0_ == '/') {
            uc32 c = Peek();
            if (c == '#' || c == '@') {
              Advance();
              Advance();
              token = SkipSourceURLComment();
              continue;
            }
            token = SkipSingleLineComment();
            continue;
          }
          if (c0_ == '*') {
            token = SkipMultiLineComment();
            continue;
          }
          if (c0_ == '=') return Select(Token::ASSIGN_DIV);
          return Token::DIV;

        case Token::BIT_AND:
          // & && &= &&=
          Advance();
          if (c0_ == '&') return Select('=', Token::ASSIGN_AND, Token::AND);
          if (c0_ == '=') return Select(Token::ASSIGN_BIT_AND);
          return Token::BIT_AND;

        case Token::BIT_OR:
          // | || |= ||=
          Advance();
          if (c0_ == '|') return Select('=', Token::ASSIGN_OR, Token::OR);
          if (c0_ == '=') return Select(Token::ASSIGN_BIT_OR);
          return Token::BIT_OR;

        case Token::BIT_XOR:
          // ^ ^=
          return Select('=', Token::ASSIGN_BIT_XOR, Token::BIT_XOR);

        case Token::PERIOD:
          // . Number
          Advance();
          if (IsDecimalDigit(c0_)) return ScanNumber(true);
          if (c0_ == '.') {
            if (Peek() == '.') {
              Advance();
              Advance();
              return Token::ELLIPSIS;
            }
          }
          return Token::PERIOD;

        case Token::TEMPLATE_SPAN:
          Advance();
          return ScanTemplateSpan();

        case Token::PRIVATE_NAME:
          if (source_pos() == 0 && Peek() == '!') {
            token = SkipSingleLineComment();
            continue;
          }
          return ScanPrivateName();

        case Token::WHITESPACE:
          token = SkipWhiteSpace();
          continue;

        case Token::NUMBER:
          return ScanNumber(false);

        case Token::IDENTIFIER:
          return ScanIdentifierOrKeyword();

        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;
}

token = one_char_tokens[c0_]可以理解为根据c0_这个字符,来初判断下当前可能的Token,具体的判断方式如下:

constexpr Token::Value GetOneCharToken(char c) {
  // clang-format off
  return
      c == '(' ? Token::LPAREN :
      c == ')' ? Token::RPAREN :
      c == '{' ? Token::LBRACE :
      c == '}' ? Token::RBRACE :
      c == '[' ? Token::LBRACK :
      c == ']' ? Token::RBRACK :
      c == '?' ? Token::CONDITIONAL :
      c == ':' ? Token::COLON :
      c == ';' ? Token::SEMICOLON :
      c == ',' ? Token::COMMA :
      c == '.' ? Token::PERIOD :
      c == '|' ? Token::BIT_OR :
      c == '&' ? Token::BIT_AND :
      c == '^' ? Token::BIT_XOR :
      c == '~' ? Token::BIT_NOT :
      c == '!' ? Token::NOT :
      c == '<' ? Token::LT :
      c == '>' ? Token::GT :
      c == '%' ? Token::MOD :
      c == '=' ? Token::ASSIGN :
      c == '+' ? Token::ADD :
      c == '-' ? Token::SUB :
      c == '*' ? Token::MUL :
      c == '/' ? Token::DIV :
      c == '#' ? Token::PRIVATE_NAME :
      c == '"' ? Token::STRING :
      c == '\'' ? Token::STRING :
      c == '`' ? Token::TEMPLATE_SPAN :
      c == '\\' ? Token::IDENTIFIER :
      // Whitespace or line terminator
      c == ' ' ? Token::WHITESPACE :
      c == '\t' ? Token::WHITESPACE :
      c == '\v' ? Token::WHITESPACE :
      c == '\f' ? Token::WHITESPACE :
      c == '\r' ? Token::WHITESPACE :
      c == '\n' ? Token::WHITESPACE :
      // IsDecimalDigit must be tested before IsAsciiIdentifier
      IsDecimalDigit(c) ? Token::NUMBER :
      IsAsciiIdentifier(c) ? Token::IDENTIFIER :
      Token::ILLEGAL;
  // clang-format on
}

如果是类似于'{'、'}'、'('、')'、';'、'^'这样的单字符Token则直接返回当前预测的Token,如果是类似'+'这样的字符,则需要使用Advance方法走一步,做以下判断

  if (c0_ == '+') return Select(Token::INC);
  if (c0_ == '=') return Select(Token::ASSIGN_ADD);
  return Token::ADD;

判断下下一个字符是'+'或者'='或者是其他,如果是其他的字符则返回预期的Token::ADD表明只是单纯的加运算,如果下一个字符是'+'则表示Token::INC自增运算,如果是'='则表示是一个Token::ASSIGN_ADD加赋值运算。
这里介绍两个函数,一个是Select函数,一个是Advance函数。Select函数有两种表现

  inline Token::Value Select(Token::Value tok) {
    Advance();
    return tok;
  }

  inline Token::Value Select(uc32 next, Token::Value then, Token::Value else_) {
    Advance();
    if (c0_ == next) {
      Advance();
      return then;
    } else {
      return else_;
    }
  }

从代码中可以看出两种都会调Advance函数,区别就在于第二中会根据next字符做一个判断,根据判断返回对应Token,类似于三项运算。
Advance函数的作用就是推进当前的解析字符,实现如下

  template <bool capture_raw = false>
  void Advance() {
    if (capture_raw) {
      AddRawLiteralChar(c0_);
    }
    c0_ = source_->Advance();
  }

这里简单的介绍下C++的模板语法,capture_raw默认是false的,但可以在掉用的时候为其赋值,语法如下,一个是传值进去,一个是使用默认的fasle。

 Advance<capture_raw>();
 Advance();

可以看到有AddRawLiteralChar这个方法,他的主要目的就是保留当前的Token的字面量到TokenDesc的raw_literal_chars中。source_->Advance()是调用的Utf16CharacterStream中的Advance方法,具体如下

  inline uc32 Advance() {
    uc32 result = Peek();
    buffer_cursor_++;
    return result;
  }

其实很简单,result里面拿到的就是上次自增过的buffer_cursor_,即需要解析的下一个字符。
在ScanSingleToken中需要特殊介绍一下的是ScanIdentifierOrKeyword和ScanString这两个函数
对于'(单引号)或"(双引号)开头的,会被认为是一个字符串,然后就会开始通过ScanString函数去解析这个字符串。

Token::Value Scanner::ScanString() {
  uc32 quote = c0_;

  next().literal_chars.Start();
  while (true) {
    AdvanceUntil([this](uc32 c0) {
      if (V8_UNLIKELY(static_cast<uint32_t>(c0) > kMaxAscii)) {
        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;
    });

    while (c0_ == '\\') {
      Advance();
      // TODO(verwaest): Check whether we can remove the additional check.
      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;
    }

    AddLiteralChar(c0_);
  }
}

在V8的代码中总会看到V8_UNLIKELY和V8_LIKELY这样的宏,这个其实是对V8条件判断的一个优化,与Linux内核中的条件优化起到了相同的功效,V8_LIKELY可以理解为在执行这个条件判断的时候很大的概率会返回true,反之V8_UNLIKELY表示很大程度上不会进入到当前的判断分支,这个优化的原理其实就是减少代码调用时候指令的跳转,将V8_LIKELY的代码在编译过程中紧跟随到当前后,从而减少指令跳转带来的性能下降。
在ScanString中的一个重要的操作就是AdvanceUntil,形参是一个函数。从函数的没命名可以猜到,这个函数要干的事情是一直推进字符的解析,直到作为参数传递进来的函数返回true时候终止。Scanner的AdvanceUntil会调用Utf16CharacterStream中的AdvanceUntil方法,这个方法具体的实现如下

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);
      }
    }
  }

整个逻辑描述下是这样的,quote是单引号或者双引号, 如果碰到了换行符或者回车符或者字符传输结束符就退出AdvanceUntil,并且在Advance过程中,把解析的字符加入到TokenDesc的literal_chars中,退出后判断退出在这些符号前的一个字符是什么,如果是\则表示之后的有可能还是字符串的继续,Advance推进一个字符后做后续操作。如果碰到了之前的quote则返回Token字符串,正常情况下会进入到下一个循环去遍历字符流,直到找到和之前记录的quote相同的字符出现,返回Token::STRING,其他意外情况下会返回Token::ILLEGAL,表示js源码中针对字符串的书写不合法。

然后介绍下另一个ScanIdentifierOrKeyword方法。主要是用来扫描一些识别符或者js关键字的

var test; //例中var是关键字(keyword),而test是一个标识符(Identifier)

在ScanSingleToken如果要调用ScanIdentifierOrKeyword首先是要通过IsIdentifierStart或者IsAsciiIdentifier来做判断的。

inline constexpr bool IsAsciiIdentifier(uc32 c) {
  return IsAlphaNumeric(c) || c == '$' || c == '_';
}

inline constexpr bool IsAlphaNumeric(uc32 c) {
  return base::IsInRange(AsciiAlphaToLower(c), 'a', 'z') || IsDecimalDigit(c);
}

bool IsIdentifierStart(uc32 c) {
  if (!base::IsInRange(c, 0, 127)) return IsIdentifierStartSlow(c);
  DCHECK_EQ(IsIdentifierStartSlow(c),
            static_cast<bool>(kAsciiCharFlags[c] & kIsIdentifierStart));
  return kAsciiCharFlags[c] & kIsIdentifierStart;
}

inline bool IsIdentifierStartSlow(uc32 c) {
  // Non-BMP characters are not supported without I18N.
  return (c <= 0xFFFF) ? unibrow::ID_Start::Is(c) : false;
} 

从上面的代码中可以看出,判断是不是关键字或者标识符主要是判断首字符,如果是a-z或A-Z或$或_或者如果不是ASCLL编码的话,比如中文,是不是在ID_Start的各类table里面,还有一个比较特殊的也作为Token::IDENTIFIER的是转义字符\,这里看到IsAlphaNumeric对于十进制的整数也会返回true,但是我们的关键字或者标识符是不能整数开头的,所以如果首字符是整数,我们会认为是Token::NUMBER直接返回的。这就是下面代码处理的原因。

     // IsDecimalDigit must be tested before IsAsciiIdentifier
      IsDecimalDigit(c) ? Token::NUMBER :
      IsAsciiIdentifier(c) ? Token::IDENTIFIER :

在ScanSingleToken中看到还有一个CombineSurrogatePair操作,这个判断的原因在于BMP(Basic Multilingual Plane)这个就是上面说的类似于中文,日文等,用了两个16位来编码Surrogate pair增补字符。CombineSurrogatePair主要的作用就是把两个16位的字符流编码合在一起。
现在已经预判断当前Token是一个Token::IDENTIFIER, 就会通过ScanIdentifierOrKeyword来对当前Token进行扫描。看下这个方法的具体实现

V8_INLINE Token::Value Scanner::ScanIdentifierOrKeyword() {
  next().literal_chars.Start();
  return ScanIdentifierOrKeywordInner();
}

ScanIdentifierOrKeyword只干了两件事,1、literal_chars.Start()就是把当前TokenDesc的literal_chars插入的position初始化到起始位置。2、调用ScanIdentifierOrKeywordInner。

V8_INLINE Token::Value Scanner::ScanIdentifierOrKeywordInner() {
  DCHECK(IsIdentifierStart(c0_));
  bool escaped = false;
  bool can_be_keyword = true;

  STATIC_ASSERT(arraysize(character_scan_flags) == kMaxAscii + 1);
  if (V8_LIKELY(static_cast<uint32_t>(c0_) <= kMaxAscii)) {
    if (V8_LIKELY(c0_ != '\\')) {
      uint8_t scan_flags = character_scan_flags[c0_];
      DCHECK(!TerminatesLiteral(scan_flags));
      STATIC_ASSERT(static_cast<uint8_t>(ScanFlags::kCannotBeKeywordStart) ==
                    static_cast<uint8_t>(ScanFlags::kCannotBeKeyword) << 1);
      scan_flags >>= 1;
      // Make sure the shifting above doesn't set IdentifierNeedsSlowPath.
      // Otherwise we'll fall into the slow path after scanning the identifier.
      DCHECK(!IdentifierNeedsSlowPath(scan_flags));
      AddLiteralChar(static_cast<char>(c0_));
      AdvanceUntil([this, &scan_flags](uc32 c0) {
        if (V8_UNLIKELY(static_cast<uint32_t>(c0) > kMaxAscii)) {
          // A non-ascii character means we need to drop through to the slow
          // path.
          // TODO(leszeks): This would be most efficient as a goto to the slow
          // path, check codegen and maybe use a bool instead.
          scan_flags |=
              static_cast<uint8_t>(ScanFlags::kIdentifierNeedsSlowPath);
          return true;
        }
        uint8_t char_flags = character_scan_flags[c0];
        scan_flags |= char_flags;
        if (TerminatesLiteral(char_flags)) {
          return true;
        } else {
          AddLiteralChar(static_cast<char>(c0));
          return false;
        }
      });

      if (V8_LIKELY(!IdentifierNeedsSlowPath(scan_flags))) {
        if (!CanBeKeyword(scan_flags)) return Token::IDENTIFIER;
        // Could be a keyword or identifier.
        Vector<const uint8_t> chars = next().literal_chars.one_byte_literal();
        return KeywordOrIdentifierToken(chars.begin(), chars.length());
      }

      can_be_keyword = CanBeKeyword(scan_flags);
    } else {
      // Special case for escapes at the start of an identifier.
      escaped = true;
      uc32 c = ScanIdentifierUnicodeEscape();
      DCHECK(!IsIdentifierStart(Invalid()));
      if (c == '\\' || !IsIdentifierStart(c)) {
        return Token::ILLEGAL;
      }
      AddLiteralChar(c);
      can_be_keyword = CharCanBeKeyword(c);
    }
  }

  return ScanIdentifierOrKeywordInnerSlow(escaped, can_be_keyword);
}

ScanIdentifierOrKeywordInner函数做的事情其实也很简单,对于ASCLL编码的Token,直接调用KeywordOrIdentifierToken来返回具体的Token,KeywordOrIdentifierToken最主要的是调用了PerfectKeywordHash::GetToken,在这个函数内部如果Token是标识符,则返回Token::IDENTIFIER,如果Token是关键字,找到对应的关键字返回关键字对应的Token Value,比如new关键字,会返回Token::NEW。如题实现如下

static const struct PerfectKeywordHashTableEntry kPerfectKeywordHashTable[64] =
    {{"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"in", Token::IN},
     {"new", Token::NEW},
     {"enum", Token::ENUM},
     {"do", Token::DO},
     {"delete", Token::DELETE},
     {"default", Token::DEFAULT},
     {"debugger", Token::DEBUGGER},
     {"interface", Token::FUTURE_STRICT_RESERVED_WORD},
     {"instanceof", Token::INSTANCEOF},
     {"if", Token::IF},
     {"get", Token::GET},
     {"set", Token::SET},
     {"const", Token::CONST},
     {"for", Token::FOR},
     {"finally", Token::FINALLY},
     {"continue", Token::CONTINUE},
     {"case", Token::CASE},
     {"catch", Token::CATCH},
     {"null", Token::NULL_LITERAL},
     {"package", Token::FUTURE_STRICT_RESERVED_WORD},
     {"false", Token::FALSE_LITERAL},
     {"async", Token::ASYNC},
     {"break", Token::BREAK},
     {"return", Token::RETURN},
     {"this", Token::THIS},
     {"throw", Token::THROW},
     {"public", Token::FUTURE_STRICT_RESERVED_WORD},
     {"static", Token::STATIC},
     {"with", Token::WITH},
     {"super", Token::SUPER},
     {"private", Token::FUTURE_STRICT_RESERVED_WORD},
     {"function", Token::FUNCTION},
     {"protected", Token::FUTURE_STRICT_RESERVED_WORD},
     {"try", Token::TRY},
     {"true", Token::TRUE_LITERAL},
     {"let", Token::LET},
     {"else", Token::ELSE},
     {"await", Token::AWAIT},
     {"while", Token::WHILE},
     {"yield", Token::YIELD},
     {"switch", Token::SWITCH},
     {"export", Token::EXPORT},
     {"extends", Token::EXTENDS},
     {"class", Token::CLASS},
     {"void", Token::VOID},
     {"import", Token::IMPORT},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"var", Token::VAR},
     {"implements", Token::FUTURE_STRICT_RESERVED_WORD},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"typeof", Token::TYPEOF},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER},
     {"", Token::IDENTIFIER}};

inline Token::Value PerfectKeywordHash::GetToken(const char* str, int len) {
  if (base::IsInRange(len, MIN_WORD_LENGTH, MAX_WORD_LENGTH)) {
    unsigned int key = Hash(str, len) & 0x3f;

    DCHECK_LT(key, arraysize(kPerfectKeywordLengthTable));
    DCHECK_LT(key, arraysize(kPerfectKeywordHashTable));
    if (len == kPerfectKeywordLengthTable[key]) {
      const char* s = kPerfectKeywordHashTable[key].name;

      while (*s != 0) {
        if (*s++ != *str++) return Token::IDENTIFIER;
      }
      return kPerfectKeywordHashTable[key].value;
    }
  }
  return Token::IDENTIFIER;
}

在ScanIdentifierOrKeywordInner中我们看到如果是IdentifierNeedsSlowPath,那会在最后调用ScanIdentifierOrKeywordInnerSlow,这个其实就是针对于BMP编码的标识符的处理,主要还是做的对字符的处理,这里不做更多的介绍了,所以js变量名最好还是写可以ASCLL编码的英文字符,会省去很多的额外处理,提高代码的性能。

总结

对于Scanner的介绍就大约这么多,本文也只是大体的介绍了下流程,具体的有许多的细节还是需要从V8源码中进行分析。对于V8的源码分析如果有一些编译原理基础的会更容易理解一些。有问题也可以和笔者一起交流分析。生成Token的步骤有了,那之后就是Parser要干的事情,解析Token生成供V8编译使用的AST(虚拟语法树)了。在下一章进行介绍。

相关文章

  • V8词法分析(Scanner)

    概述 对于js源码,需要经过一系列的操作才能让V8认识它们。在之前的概率里也有介绍,V8最初会把js源码解析成一个...

  • v8世界探险(2) - 词法和语法分析

    v8世界探险(2) - 词法和语法分析 上节我们学习了API的概况,这节开始我们就循着API来分析实现。对于解释器...

  • Javascript 语法解析

    执行过程 词法分析 -> 语法分析 -> 预编译 -> 解释执行 一. 词法分析 核心:词法分析是将字符流(cha...

  • 一个编译器最简前端的python实现

    一个编译器的前端通常包括词法分析器和语法分析器。在分析过程中,文本输入词法分析器,根据词法规则解析出词法单元。词法...

  • PL/0简单编译系统(二)

    词法分析 词法分析又称词法分析器或者扫描器,是编译程序的基本子程序之一。本项目采用手工方式设计并实现词法分析程序。...

  • 编译器 和 解释器

    一段 JS 代码是如何被 V8 引擎执行的? 程序中的一段源码在执行前都会经历三个步骤:分词/词法分析 、...

  • 编译原理笔记

    源程序分析: 词法分析 :线性分析 被称为词法分析 或者扫描比如:position=init+rate * 60 ...

  • 编译原理基础知识汇总

    前端: 词法分析 -> 语法分析 -> 语义分析后端: 生成中间代码 -> 优化 -> 生产目标代码 词法分析:有...

  • 三. Flex进阶:需要了解的一些知识

    参考:词法分析器生成工具flex词法分析器总结--flex&bison词法分析生成器flex的选项 1. Flex...

  • 基于LLVM的编译原理简明教程 (2) - 词法与语法分析初步

    递归 - 词法分析与语法分析的分界 一般来说,决定词法分析和语法分析的界限是是否需要递归。词法分析是将输入的符号流...

网友评论

      本文标题:V8词法分析(Scanner)

      本文链接:https://www.haomeiwen.com/subject/vrfooktx.html