關於compiler原理可以參考這篇文章。或者微信搜尋訂閱號'北宸南蓁'或者搜尋wDxKn89。
Parsing
我們來構建一個tokenizer用於進行lexical analysis(詞法分析)
設計思路
我們向tokenizer中傳遞需要轉換的字元型別的code,並且將code通過一些分解規則,拆分為tokens陣列。
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
複製程式碼
tokenizer程式碼實現
function tokenizer(input) {
//該'current'變數用於追蹤程式碼走到哪裡,可以類比SQL中的遊標(https://blog.csdn.net/DreamLLOver/article/details/51523887)
let current = 0;
//用於存放tokens的陣列
let tokens = [];
//通過判斷'current'與input.length的長度來控制迴圈次數和對tokens的處理次數
while (current < input.length) {
//取出指定遊標下需要處理的code.
let char = input[current];
//首先我們向要校驗是否是'(',之後會被'CallExpression'使用,但是我們現在只關心這個字元是什麼。
if (char === '(') {
//如果滿足條件,我們向token陣列push一個type值為'parn',同時value為'('的token物件。
tokens.push({
type: 'paren',
value: '(',
});
// 更新遊標的值。
current++;
// 繼續處理剩餘的code
continue;
}
//道理同上
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
//由於在程式碼中會存在空格/tab等製造的空格,同時我們在構建token的時候,是不必要關注空格的,因為空格本身對程式碼執行沒有任何影響。
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
//由於我們實現的是基於兩個數字引數的轉換,對於一些非數字會有其他匹配規則或者類似'12sdsd4'是不滿足執行規則的,所以需要挑選出滿足條件的數字token
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
//用於存放數字字元
let value = '';
//然後會不停的去loop code 序列
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 將'number'token存放到tokens中
tokens.push({ type: 'number', value });
continue;
}
//同時為了滿足compiler的多樣性,我們還支援String的轉換,只要滿足字串被double quotes 包裹
if (char === '"') {
let value = '';
// 跳過引號
char = input[++current];
//紀錄String
value += char;
char = input[++current];
}
// 跳過引號
char = input[++current];
// 新增StringToken
tokens.push({ type: 'string', value });
continue;
}
//最後我們需要處理function name,用於標識操作動作
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;
}
//最後如果code沒有滿足條件說明,該code是一個'髒code'
throw new TypeError('I dont know what this character is: ' + char);
}
//最後返回處理過的tokens
return tokens;
}
複製程式碼
Parser的程式碼實現
我們已經有了tokenizer(已經將raw code格式化為tokens array),既然烹飪材料已經有了,就需要對'材料'進行進一步加工,從而轉換為AST。
[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
複製程式碼
我們定義一個接收tokens陣列的'parser'函式
function parser(tokens) {
//與處理raw code 的方式一樣,需要一個"遊標"來跟蹤程式碼的執行軌跡
let current = 0;
//在處理raw code生成tokens是用的while對需要處理的char來根據不同的處理規格進行token的生成,現在我們處理token的時候,完全可以構建一個函式,這樣能夠使得程式碼更加的清晰
function walk() {
//獲取tokens陣列對應current下標的token物件
let token = tokens[current];
//我們根據用於標識token型別的type來進行token的分類
if (token.type === 'number') {
//更新遊標
current++;
//構建AST node結點,type是需要事先按照一定的規則進行賦值,value是token的value
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 處理type為string的token,用於生成滿足對應type的AST node
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
//根據案例code,我們是將LISP的函式轉換為C語言的,根據LISP函式的特點,'('代表函式的開始,所以我們需要處理type為paren(括號)同時值為'('的token,用於構建一個代表函式的AST node。
if (
token.type === 'paren' &&
token.value === '('
) {
//這一步需要額外的注意,我們通過處理tokens用於構建對應的AST,我們只關心函式名是什麼,引數是什麼,而不關心'('或者')'這些不具備函式特性的東西,它們只是標識一個函式的函式開始和結束
token = tokens[++current];
//構建一個type為CallExpression Base node ,我們將raw code的函式名作為該node的name的值。同時params用於存放函式內部的引數或者是子函式的nodes.
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// 剛才如果是處理'(',在處理完之後,current還是指向函式名,所以需要將遊標更新到最新
token = tokens[++current];
//由於raw code的形式是變化不定的,所以如果只是根據tokens.length來進行while處理是遠遠不夠,也不正確的,因為可能在LISP的一個函式中可能內嵌n多個子函式。
// (add 2 (subtract 4 2))
//
// 下面的程式碼中存在多個')',這種情況在parser的過程中是不可預知的。
// [
// { 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: ')' }, <<< )最外層的)
// ]
//所以,我們是不能通過簡單的迴圈來對tokens進行AST化,但是我們可以利用walk函式,進行遞迴呼叫,來讓walk來處理內嵌CallExpression的情況,只要控制好停止條件就可以。
//通過while來控制遞迴是否終止
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
//呼叫walk(),將返回的node push到用於存放AST樹形結構的node.params中。
node.params.push(walk());
token = tokens[current];
}
//更新遊標,跳出tokens的資料範圍
current++;
// 返回處理之後的node結點。
return node;
}
// 容錯處理.
throw new TypeError(token.type);
}
//構建type為Program的root node。
let ast = {
type: 'Program',
body: [],
};
//呼叫walk()來處理tokens,然後將處理之後的結果存放到AST root node的body屬性中。
while (current < tokens.length) {
ast.body.push(walk());
}
// 返回根據tokens處理過的AST物件。
return ast;
}
複製程式碼
Transformation
通過Parsing對raw code的處理,生成對應的AST。但是我們需求是對AST進行處理來生成目標AST。但是現在有一個問題,如何才能遍歷這些樹形結構,總不能用while來處理,同時也不能保證node資訊的被按訪問順序的紀錄。
所以我們需要能利用visitor(針對樹形結構的遊標)來構建一個訪問AST同時按照訪問順序紀錄一些node資訊的"遍歷器"。
traverser的程式碼實現
大致思路,traverse接收ast,還有不同型別的visitor。
traverse(ast, {
Program: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
CallExpression: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
NumberLiteral: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
});
複製程式碼
真實程式碼
function traverser(ast, visitor) {
//該函式用於處理node.body/node.params,處理頂層的樹結構,將child剝離出來,然後進行traverseNode處理
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
//接收需要被處理的'node'結點,同時將該'node'的直接parent的結點傳入
function traverseNode(node, parent) {
//根據被遍歷的node結點的type,從visitor中獲取到對應type的處理物件
let methods = visitor[node.type];
//如果該處理物件存在同時有enter方法(也就是visitor匹配了node type了),
//然後將node,node 的直接parent結點傳入,進行下一步處理
if (methods && methods.enter) {
methods.enter(node, parent);
}
//通過node type來進行不同的操作處理
switch (node.type) {
//從AST的頂層入口,頂層的type為`Program`,同時樹形的關聯關係和邏輯都被存放在body屬性中,所以我們需要對body進行traverseArray處理,由於traverseArray()內部呼叫traverseNode(),所以會對body內部所有的child進行遞迴traverseNode()處理。
case 'Program':
traverseArray(node.body, node);
break;
//對AST中的CallExpression進行處理
case 'CallExpression':
traverseArray(node.params, node);
break;
//由於`NumberLiteral` 和`StringLiteral`沒有任何child node,所以不需要進行traverseArray()處理,直接跳過
case 'NumberLiteral':
case 'StringLiteral':
break;
//容錯處理
default:
throw new TypeError(node.type);
}
//由於遍歷AST是採用depth-first,所以需要在處理完所有child的時候,進行推出操作
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
//我們通過啟動traverseNode()來觸發遍歷操作,由於頂層的AST是不存在parent node,所以直接傳入null
traverseNode(ast, null);
}
複製程式碼
transformer
通過構建了traverser(),我們現在有能力可以對AST進行有目的的遍歷,同時還可以保證他們直接存在的原有關聯不被破壞。接下來,我們就需要利用traverser()來對AST進行有目的的改造(生成新的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'
}]
}]
}]
}
複製程式碼
轉換後的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'
}]
}
}
}]
}
複製程式碼
transformer的程式碼實現(接收lisp ast)
function transformer(ast) {
//首先先構建一個 newAst和lisp ast擁有相同的program node
let newAst = {
type: 'Program',
body: [],
};
//我們採用直接在old ast中設定一個context(或者說在每一級的parent中設定一個用於接收處理過的AST),context 是從old ast轉換為new ast的引用
ast._context = newAst.body;
//呼叫traverser對ast在特定的visior下針對滿足條件的node 結點進行處理。
traverser(ast, {
// 處理type為 `NumberLiteral`的node,
NumberLiteral:
enter(node, parent) {
//重新構建了一個type為'NumberLiteral'並push到用於紀錄樹的關聯關係的context中。
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 構建處理 `StringLiteral`
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 處理 `CallExpression`.
CallExpression: {
enter(node, parent)
//構建一個新增了內建Identifier的node 結點
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
/在原始的expression node中新增一個context,用於存放函式的引數nodes
node._context = expression.arguments;
//判斷parent node的type是否是'CallExpression'(可能會存在如下的lisp (add substricl(2,3),但是是不滿足情況的,由於在ast生成的階段只是根據type來構建,沒有進行語法的校驗)
if (parent.type !== 'CallExpression') {
//我們將`CallExpression`node 包裝在ExpressionStatement中。(CallExpression在JS中保留宣告)
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
//將處理之後的node更新到parent的context中
parent._context.push(expression);
},
}
});
//返回處理之後的ast
return newAst;
}
複製程式碼
Code Generator
我們採用遞迴呼叫code generator將new ast中的node字元化。 codeGenerator的程式碼實現
function codeGenerator(node) {
// 根據type來區分不同的輸出處理
switch (node.type) {
//如果遇到`Program` node,將body的陣列中的item通過再次呼叫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);
}
}
複製程式碼
程式碼回顧
最後我們構建了一個compiler函式。他們直接的互動順序和方式如下:
- input(資料來源) => tokenizer => tokens
- tokens => parser => ast
- ast => transformer => newAst
- newAst => generator => output(目標)
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
return output;
}
複製程式碼
我們也可以將這些方法進行匯出
module.exports = {
tokenizer,
parser,
traverser,
transformer,
codeGenerator,
compiler,
};
複製程式碼