深入V8引擎-AST(3)

書生小龍發表於2019-07-03

上篇簡單介紹了入口方法的流程以及scanner類相關的部分內容,這一篇主要講scanner的初始化,即

scanner_.Initialize();

注意,這不是呼叫靜態方法。實際上Parser例項生成的時候也把scanner屬性初始化了,所以這裡可以直接用。

Parser::Parser(ParseInfo* info) : ParserBase<Parser>(/* 初始化父類的屬性 */)
scanner_(info->character_stream(), info->is_module()),/* 初始化其他屬性 */

實際上,就是初始化了scanner上的source_屬性與模組的flag,以便呼叫Initialize方法。

這個方法有點類似於libuv的非同步操作,不過當然完全不是一個東西,原始碼如下。

/**
 * 注意 這裡不做AST的全面轉換
 */
void Scanner::Initialize() {
  Init();
  next().after_line_terminator = true;
  Scan();
}

第二步我也不曉得是幹啥的,暫時不理解那個變數的意義,所以只講第一和第三步,首先是Init。

void Init() {
  Advance();
  /**
   * TokenDesc token_storage_[3];
   * 這裡做一個對映 相當於alias
   */
  current_ = &token_storage_[0];
  next_ = &token_storage_[1];
  next_next_ = &token_storage_[2];

  found_html_comment_ = false;
  scanner_error_ = MessageTemplate::kNone;
}

/**
 * source_在Parser的建構函式中初始化
 * 型別為Utf16CharacterStream 需要去那邊看實現
 */
void Advance() {
  c0_ = source_->Advance();
}

從scanner層級來看,其Advance方法的作用僅僅是對私有屬性c0_(當前字元的Unicode編碼)進行賦值,做實際操作是source_屬性上的Advance方法,而這個屬性型別為前面轉換後的Stream類(全稱是xxxCharacterStream,因為太長了,後面全部簡稱Stream類),所以具體實現需要跳到那邊去,原始碼如下。

/**
 * 從這裡開始方法域跳到了Utf16CharacterStream、BufferedCharacterStreams
 * 即Utf16CharacterStream::Advance、Utf16CharacterStream::Peek、Utf16CharacterStream::ReadBlockChecked
 */
inline uc32 Advance() {
  uc32 result = Peek();
  buffer_cursor_++;
  return result;
}

/**
 * 返回遊標所在位置的值
 * 1、已初始化
 * 2、未初始化
 * 3、已到結尾
 */
inline uc32 Peek() {
  if (V8_LIKELY(buffer_cursor_ < buffer_end_)) {
    return static_cast<uc32>(*buffer_cursor_);
  } else if (ReadBlockChecked()) {
    return static_cast<uc32>(*buffer_cursor_);
  } else {
    return kEndOfInput;
  }
}

這裡有一些東西需要解釋,首先是關於Stream類的3個遊標屬性(這個名字是我自己取的,看AST的解析總讓我想到高中的遊標卡尺),分別是buffer_start_、buffer_cursor_、buffer_end_,分別代表字元解析中的開始、當前、結束位置,在Stream類初始化時這三個屬性沒有處理,預設置0。注意,這裡的屬性指向字元,跟詞法是不同的概念,在scanner層級的三個屬性是詞法。比如說if從詞法角度講是一個,但是從字元角度來說是兩個。

下面的3個判斷註釋中給出了意義,比較有意思的是V8_LIKELY巨集,對於開發者來說算是一個無意義的巨集,但是這個巨集是給編譯器看的,表明這個分支比較有可能發生,推薦進行優化。當然,由於初始化只會走一遍,在解析未結束前大部分情況都是走第一個分支直接返回當前遊標指向的值。不過目前是第一次呼叫這個方法,我們走第二個分支。

/**
 * 這裡是做一個合法性檢測
 * 實際上只有ReadBlock做事
 */
bool ReadBlockChecked() { 
  size_t position = pos();
  USE(position);
  bool success = !has_parser_error() && ReadBlock();

  // Post-conditions: 1, We should always be at the right position.
  //                  2, Cursor should be inside the buffer.
  //                  3, We should have more characters available iff success.
  DCHECK_EQ(pos(), position);
  DCHECK_LE(buffer_cursor_, buffer_end_);
  DCHECK_LE(buffer_start_, buffer_cursor_);
  DCHECK_EQ(success, buffer_cursor_ < buffer_end_);
  return success;
}

/**
 * buffer_pos_代表當前進度位置 型別為整形
 * cursor、start作為指標指向buffer_陣列的當前、初始地址
 * 而陣列在記憶體中地址連續 且unsigned short型別佔1
 * 所以可以直接通過計算得到當前位置
 */
inline size_t pos() const {
  return buffer_pos_ + (buffer_cursor_ - buffer_start_);
}

/**
 * 1、buffer_是一個unsigned short陣列 儲存編碼處理後的單個字元
 * 2、指標start、end分別初始化為陣列的頭尾
 * 3、cursor是遊標 初始指向start
 * 例如"(function)"在buffer_表示為[40, 102, ...]
 */
bool ReadBlock() final {
  size_t position = pos();
  buffer_pos_ = position;
  buffer_start_ = &buffer_[0];
  buffer_cursor_ = buffer_start_;

  DisallowHeapAllocation no_gc;
  Range<uint8_t> range = byte_stream_.GetDataAt(position, runtime_call_stats(), &no_gc);
  if (range.length() == 0) {
    buffer_end_ = buffer_start_;
    return false;
  }

  size_t length = Min(kBufferSize, range.length());
  i::CopyCharsUnsigned(buffer_, range.start, length);
  buffer_end_ = &buffer_[length];
  return true;
}

這一塊的內容較多,實際上說多也不多。第一個方法只是純粹的檢查,保證遊標屬性的合法,pos方法則是直接通過地址計算來得到當前解析位置,原理寫在註釋裡了。

ReadBlock方法負責對Stream屬性的初始化,這個類前面沒有給出宣告,buffer_是其一個私有屬性,長度為512的short陣列。DisallowHeapAllocation不要去管,v8裡面有很多奇奇怪怪的東西,目前理解不了,當然與AST本身也毫無關係。GetDataAt比較麻煩,不想講,從結果上來講,最後返回的是字串每個字元的Unicode編碼,通過CopyCharsUnsigned方法複製到了buffer_上面,並將buffer_end_指向了最後結尾的部分。

比如說待編譯字串為"'Hello' + ' World'",經過GetDataAt處理後,會變成39, 72, ...。

這裡給一個除錯結果,buffer_初始化後,會有一堆髒資料,內容如下(長度512,只擷取了前面一部分)。

經過該方法的一系列處理,變成了

加上空格,整個字串共有18位,所以0-17的值全部被重置,後面還是老的髒資料。這些數字手動轉換一下,可以得到

剛好是待編譯的字串(先假設字串長度小於512,複雜情況後面再搞)。

至此,整個Init方法才完事,沒想到這麼長,Scan下一篇講,要幹活了。

相關文章