因爲是解析字符串,因此在開始以前介紹一下詞法結構體中關於管理字符串類的屬性。以前在TokenDesc中,有兩個屬性,以下。
struct TokenDesc {
LiteralBuffer literal_chars;
LiteralBuffer raw_literal_chars;
}複製代碼
當時沒有詳細講,主要也是比較麻煩,在這裏介紹一下該類。
class LiteralBuffer final {
public:
void AddChar(uc32 code_unit) {
if (is_one_byte()) {
if (code_unit <= static_cast<uc32>(unibrow::Latin1::kMaxChar)) {
AddOneByteChar(static_cast<byte>(code_unit));
return;
}
ConvertToTwoByte();
}
AddTwoByteChar(code_unit);
}
private:
static const int kInitialCapacity = 16;
static const int kGrowthFactor = 4;
static const int kMaxGrowth = 1 * MB;
void AddOneByteChar(byte one_byte_char) {
if (position_ >= backing_store_.length()) ExpandBuffer();
backing_store_[position_] = one_byte_char;
position_ += kOneByteSize;
}
void LiteralBuffer::ExpandBuffer() {
int min_capacity = Max(kInitialCapacity, backing_store_.length());
Vector<byte> new_store = Vector<byte>::New(NewCapacity(min_capacity));
if (position_ > 0) {
MemCopy(new_store.begin(), backing_store_.begin(), position_);
}
backing_store_.Dispose();
backing_store_ = new_store;
}
int LiteralBuffer::NewCapacity(int min_capacity) {
return min_capacity < (kMaxGrowth / (kGrowthFactor - 1))
? min_capacity * kGrowthFactor
: min_capacity + kMaxGrowth;
}
Vector<byte> backing_store_;
int position_;
bool is_one_byte_;
};複製代碼
其實原理很是簡單,用一個Vector容器去裝字符,若是容量不夠,會進行擴張。
暫時無論雙字節字符(好比中文),因此須要關注的屬性和方法就是上面的那些,有一個地方能夠關注一下,就是擴容。根據擴容機制,初始會有16 * 4的容量,當所需容量大到必定程度,會寫死,這裏來計算一下寫死的最大容量。
複製代碼
單個字符串的解析長度原來是有上限的,最大爲2mb,長度約爲200萬,此時會向Vector容量外的下標賦值,不知道會出現什麼狀況。
回到上一篇的結尾,因爲匹配到單引號,因此會走ScanString方法,源碼以下。
Token::Value Scanner::ScanString() {
uc32 quote = c0_;
next().literal_chars.Start();
while (true) {
AdvanceUntil([this](uc32 c0) {
});
while (c0_ == '\\') {
Advance();
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_);
}
}複製代碼
總的來講仍是比較簡單的,正常步進是初始化用過的Advance。代碼中有一個方法叫AdvanceUntil,從函數名判斷是一個預檢函數。這個方法調用的結構很是奇怪,C++語法我也是TM日了狗,主要做用就是預先判斷一下當前解析的字符串是否合法,整個函數結構以下。
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;
});
void AdvanceUntil(FunctionType check) {
c0_ = source_->AdvanceUntil(check);
}
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);
}
}
}複製代碼
這裏的調用方式比較邪門,其實就是JS的高階函數,函數做爲參數傳入函數,比較核心的就是find_if方法與函數參數,這裏就不講std的方法了,用JS翻譯一下,否則看起來實在太痛苦。
const callback = (str) => IsStringLiteralLineTerminator(str);
const AdvanceUntil = (callback) => {
let tarArea = buffer_.slice(buffer_cursor_, buffer_end_);
let tarIdx = tarArea.findIdx(v => callback(v));
if(tarIdx === - 1) return '非法字符串';
buffer_cursor_ = tarIdx + 1;
c0_ = buffer_[tarIdx];
}複製代碼
就是這麼簡單,變量直接對應,邏輯的話也就上面這些,find_if也就是根據索引來找符合對應條件的值。也就是說,惟一須要講解的就是字符串結束符的判斷。
涉及的新屬性有兩個,其中一個是映射數組character_scan_flags,另一個是MayTerminateString方法,二者實際上是一個東西,能夠放一塊兒看。
inline bool MayTerminateString(uint8_t scan_flags) {
return (scan_flags & static_cast<uint8_t>(ScanFlags::kStringTerminator));
}
enum class ScanFlags : uint8_t {
kTerminatesLiteral = 1 << 0,
kCannotBeKeyword = 1 << 1,
kCannotBeKeywordStart = 1 << 2,
kStringTerminator = 1 << 3,
kIdentifierNeedsSlowPath = 1 << 4,
kMultilineCommentCharacterNeedsSlowPath = 1 << 5,
};
static constexpr const uint8_t character_scan_flags[128] = {
#define CALL_GET_SCAN_FLAGS(N) GetScanFlags(N),
INT_0_TO_127_LIST(CALL_GET_SCAN_FLAGS)
#undef CALL_GET_SCAN_FLAGS
};複製代碼
首先能夠看出,character_scan_flags也是相似於以前那個Unicode與Ascii的表,對全部字符作一個映射,映射的值就是那個枚舉類型,一個字符可能對應多個可能性。這裏的計算方法能夠參照我以前那篇
利用枚舉與位運算作配置,須要哪一個屬性,就用對應的枚舉與字符映射值作與運算。
這個映射表的生成比較簡單粗暴,會對每個字符作6重或運算生成一個數,目前只看字符串終止符那塊。
constexpr uint8_t GetScanFlags(char c) {
return
| | |
((c == '\'' || c == '"' || c == '\n' || c == '\r' || c == '\\')
? static_cast<uint8_t>(ScanFlags::kStringTerminator)
: 0) | |
}複製代碼
也就是說,當前字符是單雙引號、換行與反斜槓時,會被認定多是一個字符串的結尾。
回到編譯字符串'Hello',因爲在字符結束以前,就存在另外一個單引號,因此這個符號被認爲多是結束符號賦值給了c0_,Stream類的遊標也直接移到了那個位置。至於中間的H、e、l、l、o5個字符,由於不存在任何特殊性,因此在最後的AddLiteralChar方法中被添加進了容器中。
結束後,整個函數正常返回Token::STRING做爲詞法結構體的類型,結構體的Literal_chars的容器則存儲着對應的字符串。