有史以來最小的編譯器原始碼解析
後續內容更新,請前往:個人部落格,歡迎一起交流。
前言
稍微接觸一點前端,我們都知道現在前端“ES6即正義”,然而瀏覽器的支援還處於進行階段,所以我們常常會用一個神奇的工具將 ES6 語法轉換為目前支援比較廣泛的 ES5 語法,這裡我們所說的神奇的工具就是編譯器。編譯器功能非常純粹,將字串形式的輸入語言編譯成目標語言的程式碼字串(以及sourcemap),常用的編譯器除了我們熟知的 Babel 之外,還有 gcc。不過我們今天的主角是號稱可能是有史以來最小的編譯器the-super-tiny-compiler,去掉註釋也就200多行程式碼,作者 James Kyle 更是 Babel 的活躍維護者之一。這個編譯器的功能很簡單,主要把 Lisp 風格的函式呼叫轉換成 C 風格的,例如:
Lisp 風格(轉化前) | C 風格(轉化後) | |
---|---|---|
2 + 2 | (add 2 2) | add(2, 2) |
4 - 2 | (subtract 4 2) | subtract(4, 2) |
2 + (4 - 2) | (add 2 (subtract 4 2)) | add(2, subtract(4, 2)) |
編譯器工作的三個階段
絕大多數編譯器的編譯過程都差不多,主要分為三個階段:解析:將程式碼字串解析成抽象語法樹。轉換:對抽象語法樹進行轉換操作。程式碼生成:根據轉換後的抽象語法樹生成目的碼字串。
解析
解析過程主要分為兩部分:詞法分析和語法分析。1、詞法分析是由詞法分析器把原始程式碼字串轉換成一系列詞法單元(token),詞法單元是一個數組,由一系列描述獨立語法的物件組成,它們可以是數值、標籤、標點符號、運算子、括號等。2、
我們簡單看一下 the-super-tiny-compiler 的整個解析過程:
// 原始程式碼字串 (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: ')' }, ] // 語法分析轉化後生成的抽象語法樹(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', }] }] }] }
轉換
轉換過程主要任務是修改 AST,即遍歷解析過程生成的 AST,同時進行一系列操作,比如增/刪/改節點、增/刪/改屬性、建立新樹等,我們簡單看一下 the-super-tiny-compiler 的整個轉換過程:
// 原始程式碼字串
(add 2 (subtract 4 2))
// 解析過程生成的 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',
}]
}]
}]
}
// 轉換過程生成的 AST
{
type: 'Program',
body: [{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'add'
},
arguments: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'subtract'
},
arguments: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}
}
}]
}
程式碼生成
根據轉換過程生成的抽象語法樹生成目的碼字串。
原始碼實現
接下來我們根據編譯器工作的三個階段逐一分析一下 the-super-tiny-compiler 原始碼實現。
詞法分析
詞法分析器主要任務把原始程式碼字串轉換成一系列詞法單元(token)。
// 詞法分析器 引數:程式碼字串input
function tokenizer(input) {
// 當前正在處理的字元索引
let current = 0;
// 詞法單元陣列
let tokens = [];
// 遍歷字串,獲得詞法單元陣列
while (current < input.length) {
let char = input[current];
// 匹配左括號
if (char === '(') {
// type 為 'paren',value 為左圓括號的物件
tokens.push({
type: 'paren',
value: '('
});
// current 自增
current++;
// 結束本次迴圈,進入下一次迴圈
continue;
}
// 匹配右括號
if (char === ')') {
tokens.push({
type: 'paren',
value: ')'
});
current++;
continue;
}
// \s:匹配任何空白字元,包括空格、製表符、換頁符、換行符、垂直製表符等
let WHITESPACE = /\s/;
// 跳過空白字元
if (WHITESPACE.test(char)) {
current++;
continue;
}
// [0-9]:匹配一個數字字元
let NUMBERS = /[0-9]/;
// 匹配數值
if (NUMBERS.test(char)) {
let value = '';
// 匹配連續數字,作為數值
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({
type: 'number',
value
});
continue;
}
// 匹配形如"abc"的字串
if (char === '"') {
let value = '';
// 跳躍左雙引號
char = input[++current];
// 獲取兩個雙引號之間的所有字元
while (char !== '"') {
value += char;
char = input[++current];
}
// 跳躍右雙引號
char = input[++current];
tokens.push({
type: 'string',
value
});
continue;
}
// [a-z]:匹配1個小寫字元 i 模式中的字元將同時匹配大小寫字母
let LETTERS = /[a-z]/i;
// 匹配函式名,要求只含大小寫字母
if (LETTERS.test(char)) {
let value = '';
// 獲取連續字元
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({
type: 'name',
value
});
continue;
}
// 無法識別的字元,丟擲錯誤提示
throw new TypeError('I dont know what this character is: ' + char);
}
// 詞法分析器的最後返回詞法單元陣列
return tokens;
}
通過遍歷程式碼字串,分揀出各個詞素,然後構成由一系列描述獨立語法的物件組成的陣列的詞法單元。
語法分析
語法分析器主要任務是將詞法分析器生成的詞法單元轉化為能夠描述語法結構(包括語法成分及其關係)的中間表示形式(Intermediate Representation)或抽象語法樹(Abstract Syntax Tree)。
// 語法分析器 引數:詞法單元陣列
function parser(tokens) {
// 當前正在處理的 token 索引
let current = 0;
// 遞迴遍歷(因為函式呼叫允許巢狀),將 token 轉成 AST 節點
function walk() {
// 獲取當前 token
let token = tokens[current];
// 數值
if (token.type === 'number') {
// current 自增
current++;
// 生成一個 AST節點 'NumberLiteral',用來表示數值字面量
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 字串
if (token.type === 'string') {
current++;
// 生成一個 AST節點 'StringLiteral',用來表示字串字面量
return {
type: 'StringLiteral',
value: token.value,
};
}
// 函式
if (token.type === 'paren' && token.value === '(') {
// 跳過左括號,獲取下一個 token 作為函式名
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: []
};
// 再次自增 `current` 變數,獲取引數 token
token = tokens[++current];
// 右括號之前的所有token都屬於引數
while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {
node.params.push(walk());
token = tokens[current];
}
// 跳過右括號
current++;
return node;
}
// 無法識別的字元,丟擲錯誤提示
throw new TypeError(token.type);
}
// AST的根節點
let ast = {
type: 'Program',
body: [],
};
// 填充ast.body
while (current < tokens.length) {
ast.body.push(walk());
}
// 最後返回ast
return ast;
}
通過遞迴來將詞法分析器生成的詞法單元轉化為能夠描述語法結構的 ast。
遍歷
// 遍歷器
function traverser(ast, visitor) {
// 遍歷 AST節點陣列 對陣列中的每一個元素呼叫 `traverseNode` 函式。
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// 接受一個 `node` 和它的父節點 `parent` 作為引數
function traverseNode(node, parent) {
// 從 visitor 獲取對應方法的物件
let methods = visitor[node.type];
// 通過 visitor 對應方法操作當前 node
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
// 根節點
case 'Program':
traverseArray(node.body, node);
break;
// 函式呼叫
case 'CallExpression':
traverseArray(node.params, node);
break;
// 數值和字串,不用處理
case 'NumberLiteral':
case 'StringLiteral':
break;
// 無法識別的字元,丟擲錯誤提示
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 開始遍歷
traverseNode(ast, null);
}
通過遞迴遍歷 AST,在遍歷過程中通過 visitor 對應方法操作當前 node,這裡和切面差不多。
轉換
// 轉化器,引數:AST
function transformer(ast) {
// 建立 `newAST`,它與之前的 AST 類似,Program:新AST的根節點
let newAst = {
type: 'Program',
body: [],
};
// 通過 _context 維護新舊 AST,注意 _context 是一個引用,從舊的 AST 到新的 AST。
ast._context = newAst.body;
// 通過遍歷器遍歷 引數:AST 和 visitor
traverser(ast, {
// 數值,直接原樣插入新AST
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 字串,直接原樣插入新AST
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 函式呼叫
CallExpression: {
enter(node, parent) {
// 建立不同的AST節點
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 函式呼叫有子類,建立節點對應關係,供子節點使用
node._context = expression.arguments;
// 頂層函式呼叫算是語句,包裝成特殊的AST節點
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
},
}
});
// 最後返回新 AST
return newAst;
}
這裡通過 _context 引用維護新舊 AST,簡單方便,但會汙染舊AST。
程式碼生成
// 程式碼生成器 引數:新 AST
function codeGenerator(node) {
switch (node.type) {
// 遍歷 body 屬性中的節點,且遞迴呼叫 codeGenerator,結果按行輸出
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 表示式,處理表達式內容,並用分號結尾
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';'
);
// 函式呼叫,新增左右括號,引數用逗號隔開
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 識別符號,數值,原樣輸出
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
// 字串,用雙引號包起來再輸出
case 'StringLiteral':
return '"' + node.value + '"';
// 無法識別的字元,丟擲錯誤提示
default:
throw new TypeError(node.type);
}
}
根據轉換後的新AST生成目的碼字串。
編譯器
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
return output;
}
編譯器整個工作流程:1、input => tokenizer => tokens2、tokens => parser => ast3、ast => transformer => newAst4、newAst => generator => output將上面流程串起來,就構成了簡單的編譯器。