事物的特殊性往往也能反映事物的普遍性。-- 網摘
背景:在瞭解抽象語法樹的時候偶然瞭解到 the-super-tiny-compiler這個開源項目,一看之下似乎不復雜,原文全是英文,閱讀的時候就想到翻譯一下,一方面加深理解,一方面也作爲閱讀的成果。
本文主要是對源碼中註釋的翻譯。
大多數編譯器分爲三個主要階段
- 解析
- 轉換
- 生成代碼
一、解析
定義:將原始代碼轉成更抽象的表示。
詳細解釋:解析通常分爲詞法分析和句法分析兩個階段。
詞法分析:由詞法分析器將原始代碼分解成不同的標記,這些標記包括數字、標籤、標點符號、運算符等等,這取決於你怎麼去定義詞法分析器。
句法分析:將詞法分析器產出的標記重新格式化爲一種表示形式,該表示形式描述了語法的每個部分以及它們之間的關係。 這被稱爲中間表示或抽象語法樹。抽象語法樹,簡稱AST,是一個深層嵌套的對象,以易於使用的方式表示代碼,並告訴我們很多信息。
舉例:
類LISP語法:(add 2 (subtract 4 2))
詞法解析後可能是這樣:
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
句法解析後可能是這樣:
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2',
},
{
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4',
}, {
type: 'NumberLiteral',
value: '2',
}]
}]
}]
}
二、轉換
定義:利用代碼更抽象的表示法添加編譯器想做的任何操作。
詳細解釋:
1、變動:對拿到的AST做一些需要的改變,AST由很多語法節點組成,我們可以增加、刪除、替換、更新樹上的節點,當我們最終的目標語言是另一種語言時,也可以基於現有的語法樹創建新的語法樹。
2、遍歷:採用深度優先遍歷AST來獲取節點。
比如有如下AST:
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}]
}]
}
遍歷順序:
- Program
- CallExpression (add)
- NumberLiteral (2)
- CallExpression (subtract)
- NumberLiteral (4)
- NumberLiteral (2)
3、訪問器:用於操作AST的節點
訪問器模型:
/*
* -> Program (enter)
* -> CallExpression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Call Expression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Number Literal (enter)
* <- Number Literal (exit)
* <- CallExpression (exit)
* <- CallExpression (exit)
* <- Program (exit)
*/
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
}
};
三、生成代碼
定義:將轉換後的代碼表示轉化成新的源代碼。
詳細解釋:實際上,代碼生成器要知道怎麼去輸出AST中所有不同類型的節點,它要遞歸地調用自身去輸出嵌套的節點,直到所有節點都被輸出到一個長的存儲代碼的字符串中。
小結
以上就是一個編譯器不同的部分。當然,也不是說所有的編譯器就是上面描述的那樣,基於不同的目的,編譯器可能會有更多的步驟。
源碼