
概述
对于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(虚拟语法树)了。在下一章进行介绍。
网友评论