## 如何使用 JavaScript 實現一門編程語言(4) —— Token stream
tokenizer(標記器, 也稱為“詞法分析器”), 對?[字符輸入流](http://annn.me/implement-a-programming-language-3)?進行操作,并返回具有相同接口的流對象,但由 peek()/next() 返回的值一個個token。token是具有兩個屬性的對象:type和value。以下是我們所支持的token的一些示例:
```
{ type:“ punc ”,value:“ (” } // 標點符號:parens,逗號,分號等等
{ type:“ num ”,value:5 } // numbers
{ type:“ str ”,value:“ Hello World !“ } // 字符串
{ type:” kw “,value:”lambda “ } // keywords
{ type: ” var “, value: ” a “ } // 標識符
{ type: ” op “, value: ” != “ } // 運算符
```
空白符和注釋將被跳過,沒有令牌返回。
為了編寫tokenizer,我們需要更仔細地認識我們的語言的語法。有個辦法是,根據當前字符(由input.peek()返回的)來決定讀取哪種類型的token:
1. 首先,跳過空格。
2. 如果然后返回。input.eof()null
3. 如果它是一個井號(#),則跳過注釋(在行結束后重試)。
4. 如果它是一個引號,那么閱讀一個字符串。
5. 如果它是一個數字,那么我們繼續閱讀一個數字。
6. 如果它是“字母”,則讀取標識符或關鍵字token。
7. 如果它是標點符號之一,則返回標點符號token。
8. 如果它是運算符,則返回運算符token。
9. 如果以上都不是,那就拋出錯誤了。input.croak()
“read_next”函數作為tokenizer的核心部分 ,它實現了上面的內容:
```
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不會被看作是三個token,而是作為一個單一的標識符(一個token)。這條規則的原因是我希望能夠定義名為is-pair或者string>=的函數(對不起,這就是我的Lisper)。
此外,read_ident()函數將檢查已知關鍵字列表中的標識符,如果它存在,它將返回一個”kw”令牌.
這里是我們語言的完整tokenizer:
```
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(),因為它可能在之前被peek過(在這種情況下,read_next()已經被調用并且stream被提前)。因此我們需要一個current變量跟蹤當前token。
- 我們只支持十進制數與通常的符號(沒有1E5的東西,沒有十六進制,沒有八進制)。但是如果我們需要更多,這些更改只能在read_number()中進行,并且很容易實現。
- 與JavaScript不同,唯一不能在字符串中引用的字符是引號字符本身和反斜杠。你需要反斜杠轉義他們。而且,字符串可能包含換行符,制表符等。我們太長不會解析像\n,\t等的轉義。
現在,我們有足夠強大的工具來方便地編寫解析器(parser)了,但我建議您首先看下?[AST](http://annn.me/implement-a-programming-language-5)?的描述。
- Web 開發筆記
- 從輸入 URL 到頁面加載完成的過程中都發生了什么事情?
- 從瀏覽器接收url到開啟網絡請求線程
- 開啟網絡線程到發出一個完整的http請求
- 從服務器接收到請求到對應后臺接收到請求
- 后臺和前臺的http交互
- http的緩存
- 解析頁面流程
- HTML解析,構建DOM
- CSS解析,構建CSSOM
- 資源外鏈的下載
- CSS的可視化格式模型
- 包含塊(Containing Block)
- 控制框(Controlling Box)
- BFC(Block Formatting Context)
- IFC(Inline Formatting Context)
- 其它
- JS引擎解析過程
- JS的解釋階段
- JS的預處理階段
- JS的執行階段
- 回收機制
- 參考資料
- JavaScript模塊化編程
- AMD
- requireJS
- CommonJS
- UMD
- ES6模塊
- 參考資料
- 使用 JavaScript 實現一門編程語言
- 如何使用 JavaScript 實現一門編程語言(1) —— 前言
- 如何使用 JavaScript 實現一門編程語言(2) —— 編寫一個解析器
- 如何使用 JavaScript 實現一門編程語言(3) —— Input stream
- 如何使用 JavaScript 實現一門編程語言(4) —— Token stream
- 如何使用 JavaScript 實現一門編程語言(5) —— AST
- 如何使用 JavaScript 實現一門編程語言(6) —— Interpreter
- 完整代碼
- 參考資料
- 前端布局概論
- 參考資料
- Windows 筆記
- 錯誤解決
- win10應用商店無法登錄提示0x80070426解決方法
- 使用技巧
- 設置 Hyper-V 和 VMware 共存
- Powershell
- WSL