在前面的課程中,我在 JavaScript 和 CSS 的部分,多次提到了編譯原理相關的知識。這一部分的知識,如果我們從編譯原理“龍書”等正規的資料中學習,就會耗費掉不少的時間,所以我在這裏設計了一個小實驗,幫助你快速理解編譯原理相關的知識。
今天的內容比較特殊,我們來做一段詳細的代碼實驗,詳細的代碼我放在了文章裏,如果你正在收聽音頻,可以點擊文章查看詳情。
分析
按照編譯原理相關的知識,我們來設計一下工作,這裏我們分成幾個步驟。
-
定義四則運算:產出四則運算的詞法定義和語法定義。
-
詞法分析:把輸入的字符串流變成 token。
-
語法分析:把 token 變成抽象語法樹 AST。
-
解釋執行:後序遍歷 AST,執行得出結果。
定義四則運算
四則運算就是加減乘除四種運算,例如:
1 + 2 * 3
首先我們來定義詞法,四則運算裏面只有數字和運算符,所以定義很簡單,但是我們還要注意空格和換行符,所以詞法定義大概是下面這樣的。
-
Token
-
Number: 1 2 3 4 5 6 7 8 9 0 的組合
-
Operator: + 、-、 *、 / 之一
-
-
Whitespace:
<sp>
-
LineTerminator:
<LF>
<CR>
這裏我們對空白和換行符沒有任何的處理,所以詞法分析階段會直接丟棄。
接下來我們來定義語法,語法定義多數採用 BNF,但是其實大家寫起來都是亂寫的,比如 JavaScript 標準裏面就是一種跟 BNF 類似的自創語法。
不過語法定義的核心思想不會變,都是幾種結構的組合產生一個新的結構,所以語法定義也叫語法產生式。
因爲加減乘除有優先級,所以我們可以認爲加法是由若干個乘法再由加號或者減號連接成的:
<Expression> ::=
<AdditiveExpression><EOF>
<AdditiveExpression> ::=
<MultiplicativeExpression>
|<AdditiveExpression><+><MultiplicativeExpression>
|<AdditiveExpression><-><MultiplicativeExpression>
這種 BNF 的寫法類似遞歸的原理,你可以理解一下,它表示一個列表。爲了方便,我們把普通數字也得當成乘法的一種特例了。
<MultiplicativeExpression> ::=
<Number>
|<MultiplicativeExpression><*><Number>
|<MultiplicativeExpression></><Number>
好了,這就是四則運算的定義了。
詞法分析:狀態機
詞法分析部分,我們把字符流變成 token 流。詞法分析有兩種方案,一種是狀態機,一種是正則表達式,它們是等效的,選擇你喜歡的就好,這裏我都會你介紹一下狀態機。
根據分析,我們可能產生四種輸入元素,其中只有兩種 token,我們狀態機的第一個狀態就是根據第一個輸入字符來判斷進入了哪種狀態:
var token = [];const start = char => { if(char === '1'
|| char === '2'
|| char === '3'
|| char === '4'
|| char === '5'
|| char === '6'
|| char === '7'
|| char === '8'
|| char === '9'
|| char === '0'
) {
token.push(char); return inNumber;
} if(char === '+'
|| char === '-'
|| char === '*'
|| char === '/'
) { emmitToken(char, char); return start
} if(char === ' ') { return start;
} if(char === '\r'
|| char === '\n'
) { return start;
}
}const inNumber = char => { if(char === '1'
|| char === '2'
|| char === '3'
|| char === '4'
|| char === '5'
|| char === '6'
|| char === '7'
|| char === '8'
|| char === '9'
|| char === '0'
) {
token.push(char); return inNumber;
} else { emmitToken("Number", token.join(""));
token = []; return start(char); // put back char
}
}
這個狀態機非常簡單,它只有兩個狀態,因爲我們只有 Number 不是單字符的 token。
這裏我的狀態機實現是非常經典的方式:用函數表示狀態,用 if 表示狀態的遷移關係,用 return 值表示下一個狀態。
下面我們來運行一下這個狀態機試試看:
function emmitToken(type, value) { console.log(value);
}var input = "1024 + 2 * 256"var state = start;for(var c of input.split(''))
state = state(c);state(Symbol('EOF'))
運行後我們發現輸出如下:
1024 + 2 * 256
這是我們想要的答案。
語法分析:LL
做完了詞法分析,我們開始進行語法分析,LL 語法分析根據每一個產生式來寫一個函數,首先我們來寫好函數名:
function AdditiveExpression( ){
}function MultiplicativeExpression(){
}
爲了便於理解,我們就不做流式處理了,實際上一般編譯代碼都應該支持流式處理。
所以我們假設 token 已經都拿到了:
var tokens = [{ type:"Number", value: "1024"}, { type:"+"
value: "+"}, { type:"Number", value: "2"}, { type:"*"
value: "*"}, { type:"Number", value: "256"}, { type:"EOF"}];
每個產生式對應着一個函數,例如:根據產生式,我們的 AdditiveExpression 需要處理三種情況:
<AdditiveExpression> ::=
<MultiplicativeExpression>
|<AdditiveExpression><+><MultiplicativeExpression>
|<AdditiveExpression><-><MultiplicativeExpression>
那麼 AddititveExpression 中就要寫三個 if 分支,來處理三種情況。
AdditiveExpression 的寫法是根傳入的節點,利用產生式合成新的節點
function AdditiveExpression(source){ if(source[0].type === "MultiplicativeExpression") { let node = { type:"AdditiveExpression", children:[source[0]]
}
source[0] = node; return node;
}
if(source[0].type === "AdditiveExpression" && source[1].type === "+") { let node = { type:"AdditiveExpression", operator:"+", children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
}
source.unshift(node);
} if(source[0].type === "AdditiveExpression" && source[1].type === "-") { let node = { type:"AdditiveExpression", operator:"-", children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
}
source.unshift(node);
}
}
那麼下一步我們就把解析好的 token 傳給我們的頂層處理函數 Expression。
Expression(tokens);
接下來,我們看 Expression 該怎麼處理它。
我們 Expression 收到第一個 token,是個 Number,這個時候,Expression 就傻了,這是因爲產生式只告訴我們,收到了 AdditiveExpression 怎麼辦。
這個時候,我們就需要對產生式的首項層層展開,根據所有可能性調用相應的處理函數,這個過程在編譯原理中稱爲求“closure”。
function Expression(source){ if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "EOF" ) { let node = { type:"Expression", children:[source.shift(), source.shift()]
}
source.unshift(node); return node;
} AdditiveExpression(source); return Expression(source);
}function AdditiveExpression(source){ if(source[0].type === "MultiplicativeExpression") { let node = { type:"AdditiveExpression", children:[source[0]]
}
source[0] = node; return AdditiveExpression(source);
}
if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "+") { let node = { type:"AdditiveExpression", operator:"+", children:[]
}
node.children.push(source.shift());
node.children.push(source.shift()); MultiplicativeExpression(source);
node.children.push(source.shift());
source.unshift(node); return AdditiveExpression(source);
} if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "-") { let node = { type:"AdditiveExpression", operator:"-", children:[]
}
node.children.push(source.shift());
node.children.push(source.shift()); MultiplicativeExpression(source);
node.children.push(source.shift());
source.unshift(node); return AdditiveExpression(source);
} if(source[0].type === "AdditiveExpression") return source[0]; MultiplicativeExpression(source); return AdditiveExpression(source);
}function MultiplicativeExpression(source){ if(source[0].type === "Number") { let node = { type:"MultiplicativeExpression", children:[source[0]]
}
source[0] = node; return MultiplicativeExpression(source);
}
if(source[0].type === "MultiplicativeExpression" && source[1] && source[1].type === "*") { let node = { type:"MultiplicativeExpression", operator:"*", children:[]
}
node.children.push(source.shift());
node.children.push(source.shift());
node.children.push(source.shift());
source.unshift(node); return MultiplicativeExpression(source);
} if(source[0].type === "MultiplicativeExpression"&& source[1] && source[1].type === "/") { let node = { type:"MultiplicativeExpression", operator:"/", children:[]
}
node.children.push(source.shift());
node.children.push(source.shift());
node.children.push(source.shift());
source.unshift(node); return MultiplicativeExpression(source);
} if(source[0].type === "MultiplicativeExpression") return source[0]; return MultiplicativeExpression(source);
};var source = [{ type:"Number", value: "3"}, { type:"*", value: "*"}, { type:"Number", value: "300"}, { type:"+", value: "+"}, { type:"Number", value: "2"}, { type:"*", value: "*"}, { type:"Number", value: "256"}, { type:"EOF"}];var ast = Expression(source);console.log(ast);
解釋執行
得到了 AST 之後,最困難的一步我們已經解決了。這裏我們就不對這顆樹做任何的優化和精簡了,那麼接下來,直接進入執行階段。我們只需要對這個樹做遍歷操作執行即可。
我們根據不同的節點類型和其它信息,寫 if 分別處理即可:
function evaluate(node) { if(node.type === "Expression") { return evaluate(node.children[0])
} if(node.type === "AdditiveExpression") { if(node.operator === '-') { return evaluate(node.children[0]) - evaluate(node.children[2]);
} if(node.operator === '+') { return evaluate(node.children[0]) + evaluate(node.children[2]);
} return evaluate(node.children[0])
} if(node.type === "MultiplicativeExpression") { if(node.operator === '*') { return evaluate(node.children[0]) * evaluate(node.children[2]);
} if(node.operator === '/') { return evaluate(node.children[0]) / evaluate(node.children[2]);
} return evaluate(node.children[0])
} if(node.type === "Number") { return Number(node.value);
}
}