目錄
- 用javascript實現一門程式語言-前言
- 用javascript實現一門程式語言-語言構想
- 用javascript實現一門程式語言-寫一個解析器
- 用javascript實現一門程式語言-字元輸入流
- 用javascript實現一門程式語言-詞法分析
詞法分析(the token input stream)
詞法分析是基於字元輸入流進行操作的,但是通過peek() 或 next() 返回的是一個特殊物件,即token
. 一個token
中包含兩個屬性: type
和 value
. 以下是幾個例子:
{ type: "punc", value: "(" } // 標點符號(punctuation): 括號(parens), 逗號(comma), 分號(semicolon) etc.
{ type: "num", value: 5 } // 數字
{ type: "str", value: "Hello World!" } // 字串
{ type: "kw", value: "lambda" } // 關鍵字(keywords)
{ type: "var", value: "a" } // 變數名(identifiers)
{ type: "op", value: "!=" } // 操作(operators)
複製程式碼
空白和註釋會被直接跳過,沒有token返回.
為了完成詞法分析器,我們需要對語法瞭解的很詳細.我們需要對peek()返回的當前字元進行處理,返回token,有以下幾點需要注意:
- 跳過空格
- 如果到達末尾,返回null
- 如果遇見#,跳過註釋,即本行後面所有內容
- 如果是引號,讀入字串
- 如果是數字,讀入數字
- 如果是一個單詞,按關鍵字或者變數處理
- 如果是標點符號,返回標點符號的token
- 如果是操作符,返回操作符的token
- 如果不匹配上面任何一個,輸出錯誤
input.croak()
下面是詞法分析的核心程式碼-讀取下一個:
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
.這裡面用到了很多工具函式,例如read_string(), read_number()等等.我們沒必要在這裡就把這些函式寫出來增加複雜度.
另一個需要注意的是,我們不會一下子就去拿到所有的輸入流,每次解析器只會讀取下一個token,這樣便於我們去定位錯誤(有時因為語法錯誤,解析器不用繼續解析).
read_ident()
函式會盡可能多的讀取可以作為變數名稱的字元作為變數名.變數名必須以字母,λ或_開頭,可以包含字母,數字或者?!-<>=
.因此foo-bar
不會作為3個token
讀入,而是作為一個變數.定義這個規則的原因是為了讓我定義is-pair
這樣的變數.
當然,read_ident()
函式也會去檢查讀入的名稱是不是一個關鍵字.如果是關鍵字將會返回kw token
, 否則返回var token
.
以下是TokenStream
的所有程式碼:
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 "
".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 != "
" });
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()
,因為可能提前呼叫過read_next()
,所以,在current存在的時候就直接返回current就可以了.- 我們只支援十進位制數字,不支援科學計數法,不支援16進位制,8進位制.如果我們需要這些的話,就在
read_number()
函式中新增處理方法就可以了 - 與javascript不同的是,字串中不能包含引號本身和反斜槓,但是我們不會影響常用的轉義字元
下面一節會介紹一下AST
.