詞法分析器執行在字元輸入流
之上,通過相同的介面返回一個流物件,但是通過peek()
/next()
返回的值是tokens
。一個token
是一個物件,包含兩個屬性:type
和value
。下面是一些支援tokens
的例子:
{ type: "punc", value: "(" } // 標點符號: 括號、逗號、分號等等。
{ type: "num", value: 5 } // 數字
{ type: "str", value: "Hello World!" } // 字串
{ type: "kw", value: "lambda" } // 關鍵字
{ type: "var", value: "a" } // 識別符號
{ type: "op", value: "!=" } // 運算子複製程式碼
空格和評論會被跳過,不會返回tokens
。
為了寫詞法分析器,我們需要深入研究我們語言的語法。要注意當前字元(通過input.peek()
返回),我們需要怎樣處理:
首先,跳過空格。
If
input.eof()
then returnnull
.如果是井號 (
#
),跳過評論。如果是引號,識別成字串。
如果是數字,我們就處理成數字。
如果是字母,就處理成識別符號或者關鍵詞。
如果是一個標點符號,返回一個標點符號
token
。如果是運算子,返回運算子
token
。如果以上都沒有就通過
input.croak()
丟擲異常。
下面是“read_next”函式 —— 詞法分析器的核心 :
function read_next() {
read_while(is_whitespace);
if (input.eof()) return null;
var ch = input.peek();
if (ch == "#") {
skip_comment();
return read_next();
}
if (ch == '"') return read_string();
if (is_digit(ch)) return read_number();
if (is_id_start(ch)) return read_ident();
if (is_punc(ch)) return {
type : "punc",
value : input.next()
};
if (is_op_char(ch)) return {
type : "op",
value : read_while(is_op_char)
};
input.croak("Can't handle character: " + ch);
}複製程式碼
這是一個“排程”函式,next()
方法用來接收下一個token。注意它使用很多工具來聚焦特殊的token型別,比如read_string()
, read_number()
等等。雖然很多函式都沒有呼叫過,但是並不是想把“排程”複雜化。
另外一個需要注意的是我們沒有一次將輸入的流處理完。每次編譯器只呼叫下一個token,我們讀取一個token。如果解析出錯我們甚至到不了流的末尾。
只要它們允許作為一個識別符號(is_id
)的一部分,read_ident()
將一直讀取字元。識別符號必須以字元,λ或者_開頭,後面可以跟隨數字,或者?!-=。因此 foo-bar不會被識別成三個tokens,只會識別成一個"var"token。
同時,read_ident()
函式將比對已知關鍵詞列表檢查識別符號,以及如果在列表中將返回"kw"
token,而不是"var"
token。
我認為程式碼可以很好的說清楚自己是什麼,所以下面是我們語言已完成的詞法分析器程式碼。末尾有幾個小提示:
function TokenStream(input) {
var current = null;
var keywords = " if then else lambda λ true false ";
return {
next : next,
peek : peek,
eof : eof,
croak : input.croak
};
function is_keyword(x) {
return keywords.indexOf(" " + x + " ") >= 0;
}
function is_digit(ch) {
return /[0-9]/i.test(ch);
}
function is_id_start(ch) {
return /[a-zλ_]/i.test(ch);
}
function is_id(ch) {
return is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0;
}
function is_op_char(ch) {
return "+-*/%=&|<>!".indexOf(ch) >= 0;
}
function is_punc(ch) {
return ",;(){}[]".indexOf(ch) >= 0;
}
function is_whitespace(ch) {
return " \t\n".indexOf(ch) >= 0;
}
function read_while(predicate) {
var str = "";
while (!input.eof() && predicate(input.peek()))
str += input.next();
return str;
}
function read_number() {
var has_dot = false;
var number = read_while(function(ch){
if (ch == ".") {
if (has_dot) return false;
has_dot = true;
return true;
}
return is_digit(ch);
});
return { type: "num", value: parseFloat(number) };
}
function read_ident() {
var id = read_while(is_id);
return {
type : is_keyword(id) ? "kw" : "var",
value : id
};
}
function read_escaped(end) {
var escaped = false, str = "";
input.next();
while (!input.eof()) {
var ch = input.next();
if (escaped) {
str += ch;
escaped = false;
} else if (ch == "\\") {
escaped = true;
} else if (ch == end) {
break;
} else {
str += ch;
}
}
return str;
}
function read_string() {
return { type: "str", value: read_escaped('"') };
}
function skip_comment() {
read_while(function(ch){ return ch != "\n" });
input.next();
}
function read_next() {
read_while(is_whitespace);
if (input.eof()) return null;
var ch = input.peek();
if (ch == "#") {
skip_comment();
return read_next();
}
if (ch == '"') return read_string();
if (is_digit(ch)) return read_number();
if (is_id_start(ch)) return read_ident();
if (is_punc(ch)) return {
type : "punc",
value : input.next()
};
if (is_op_char(ch)) return {
type : "op",
value : read_while(is_op_char)
};
input.croak("Can't handle character: " + ch);
}
function peek() {
return current || (current = read_next());
}
function next() {
var tok = current;
current = null;
return tok || read_next();
}
function eof() {
return peek() == null;
}
}複製程式碼
next()
函式不會總去呼叫read_next()
,我們需要一個current
變數來一直跟蹤當前的token。我們只使用常用符號來支援小數(沒有1E5這種寫法,沒有十六進位制,沒有八進位制)。但是如果我們需要更多,我們只能去
read_number()
中修改,改起來很容易。不像JavaScript,在字串中,引號字元和反斜槓字元是唯一需要加引號的字元。
我們現在已經有足夠的工具來輕鬆實現解析器了,在此之前我建議你最好先去熟悉下我們的抽象語法樹。