编译器工作流
-
解析(Parsing)解析是将最初原始的代码转换为一种更加抽象的表示(即 AST)
-
转换(Transformation)转换将对这个抽象的表示做一些处理,让它能做到编译器期望它做到的事情
-
代码生成(Code Generation)接收处理之后的代码表示,然后把它转换成新的代码
解析(Parsing)
-
解析一般来说会分成两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)
-
词法分析接收原始代码,然后把它分割成一些被称为
token
的东西,这个过程是在词法分析器(Tokenizer或者Lexer)中完成的 -
Token 是一个数组,由一些代码语句的碎片组成。它们可以是数字、标签、标点符号、运算符或者其它任何东西
-
语法分析 接收之前生成的
token
,把它们转换成一种抽象的表示,这种抽象的表示描述了代码语句中的每一个片段以及它们之间的关系。这被称为中间表示(intermediate representation)或抽象语法树(Abstract Syntax Tree, 缩写为AST) -
抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们更多信息
例如原本的jsx代码
<h1 id=“title”><span>hello</span>world</h1>
语法分析转换为token数组之后
[ {type: "Punctuator', value: '<'}, {type: 'JSXIdentifier', value: 'h1'}, ..... ]
可以参考esprima库得到抽象语法树
-
esprima实现
转换(Transformation)
- 编译器的下一步就是转换,它只是把 AST 拿过来然后对它做一些修改.它可以在同种语言下操作 AST,也可以把 AST 翻译成全新的语言
- 你或许注意到了我们的
AST
中有很多相似的元素,这些元素都有type
属性,它们被称为AST
结点。这些结点含有若干属性,可以用于描述 AST 的部分信息 - 比如下面是一个
NumberLiteral
结点
{
type: 'NumberLiteral',
value: '2'
}
- 又比如下面是一个
CallExpression
结点
{
type: 'CallExpression',
name: 'subtract',
params: [...nested nodes go here...]
}
- 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新的 AST
- 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对新语言的全新的 AST
遍历(Traversal)
- 为了能处理所有的结点,我们需要遍历它们,使用的是深度优先遍历
{
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 的遍历流程是这样的
Program - 从 AST 的顶部结点开始
CallExpression (add) - Program 的第一个子元素
NumberLiteral (2) - CallExpression (add) 的第一个子元素
CallExpression (subtract) - CallExpression (add) 的第二个子元素
NumberLiteral (4) - CallExpression (subtract) 的第一个子元素
NumberLiteral (2) - CallExpression (subtract) 的第二个子元素
estraverse-fb插件实现
const estraverse = require('estraverse-fb')
let indent = 0
function padding(){
return ' '.repeat(indent)
}
//深度优先遍历
estraverse.traverse(ast, {
enter(node){
console.log(padding() + node.type + '进入');
indent+=2
},
leave(node){
console.log(padding() + node.type + '离开');
indent-=2
}
})
运行结果
Program进入
ExpressionStatement进入
JSXElement进入
JSXOpeningElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXAttribute进入
JSXIdentifier进入
JSXIdentifier离开
Literal进入
Literal离开
JSXAttribute离开
JSXOpeningElement离开
JSXClosingElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXClosingElement离开
JSXElement进入
JSXOpeningElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXOpeningElement离开
JSXClosingElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXClosingElement离开
JSXText进入
JSXText离开
JSXElement离开
JSXText进入
JSXText离开
JSXElement离开
ExpressionStatement离开
Program离开
代码生成
- 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,但是代码生成最主要的部分还是根据 AST 来输出代码
- 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码表示,以便于线性地输出代码。
分词的方式
正则分词
正则分词有很多受限条件
// 正则分词例子
let RegExpObject = /([0-9]+)|([ ])|(\+)|(\-)|(\*)|(\/)|([\(])|([\)])/g;
let names = ["Number","Space","+","-","*","/","(",")"];
function* tokenize(source){
let result = null;
while(true){
result = RegExpObject.exec(source);
if(!result) break;
let token = {type:null,value:null};
let index = result.find((item,index)=>index>0&&!!item);
token.type = names[index];
token.value = (result[0]);
yield token;
}
}
let tokens = [];
for(let token of tokenize("33+44-55*66*(77+55)")){
tokens.push(token);
}
console.log(tokens);
有限状态机分词
- 每一个状态都是一个机器,每个机器都可以接收输入和计算输出
- 机器本身没有状态,每一个机器会根据输入决定下一个状态
// 有限状态机分词例子,实现 10+20+30-10 分词
/**
* 分词
* 状态机实现
*/
let tokens = [];
let NUMBERS = /[0-9]/;
//类型
const types = {
Numeric: "Numeric",
Punctuator: "Punctuator",
};
/**
* start表示开始状态函数
* 接受一个字符,返回下一个状态函数
*/
let currentToken;
function start(char) {
//第一个是1
if (NUMBERS.test(char)) {
//如果是数字,就生成新的token
currentToken = { type: types.Numeric, value: "" };
}
//进入下一个状态,收集number状态
return number(char);
}
/**
* number表示number状态的函数
*/
function number(char) {
if (NUMBERS.test(char)) {
// 当前char是number
currentToken.value += char;
//推断下一个还是Number状态
return number;
} else if (char === "+" || char === "-") {
emit(currentToken);
//将这个+/-也收集起来
emit({ type: types.Punctuator, value: char });
currentToken = { type: types.Numeric, value: "" };
return number; //继续准备收集number
}
}
/**
*
*/
function emit(token) {
//当遇到+,-的时候,就表示一个token结束了,该收集了。
tokens.push(token);
currentToken = { type: "", value: "" };
}
function tokenizer(input) {
let state = start;
for (let char of input) {
state = state(char);
}
if(currentToken.value.length > 0){
emit(currentToken)
}
}
tokenizer("10+20+30-10");
console.log(tokens);
// 打印结果是
[
{ type: 'Numeric', value: '10' },
{ type: 'Punctuator', value: '+' },
{ type: 'Numeric', value: '20' },
{ type: 'Punctuator', value: '+' },
{ type: 'Numeric', value: '30' },
{ type: 'Punctuator', value: '-' },
{ type: 'Numeric', value: '10' }
]
实现编译器
词法分析
-
首先,定义token的类型
-
然后,根据有限状态机,分割token
-
因为解析jsx,所以第一个字符必须是‘<’,接着对第一个字符分词。然后根据第一个字符的状态返回对应函数。
-
对于<后面的判断,可能是普通值,就表示他是一个jsx标识符,应该返回j对应函数。
-
如果<后面是’/’,表示他是一个标签的结束标签,就应该继续判断。
-
依次类推,根据值的不同状态,使用不同函数处理值
代码实现 //类型 const types = { leftParenthese: "leftParenthese", // < jsxIdentifier: "jsxIdentifier", //jsx元素,如span h1等 AttributeKey: "AttributeKey", // 属性key 如id Equal: "Equal", // = AttributeStringVlaue: "AttributeStringVlaue", // 属性value, 如id='xx'中的xx rightParenthese: "rightParenthese", JsxText: "JsxText", Punctuator: "Punctuator", // 反斜杠/ }; const LETTERs = /[a-z1-9]/; const tokens = []; let currentToken = { type: "", value: "" }; function emit() { if (currentToken.value.length > 0) { tokens.push(currentToken); } currentToken = { type: "", value: "" }; } // 开始的函数 function start(char) { if (char === "<") { currentToken.type = types.leftParenthese; currentToken.value = "<"; emit(); //准备接收jsx元素 return foundLeftParenthese; // 找到了 '<' } // throw new Error("第一个字符串必须是<"); } //结束了 function eof() { if (currentToken.value.length > 0) { emit(); } } // 找到<后的函数 function foundLeftParenthese(char) { if (LETTERs.test(char)) { // h // char是一个小写字母 currentToken.type = types.jsxIdentifier; currentToken.value += char; return jsxIdentifier; //返回的状态是继续收集标识符 } else if (char === "/") { // </h1>结束标签 currentToken.type = types.Punctuator; currentToken.value = "/"; emit(); return foundLeftParenthese; } else { throw new Error("foundLeftParenthese 找到<后语法报错"); } } //找到jsx后的函数 function jsxIdentifier(char) { if (LETTERs.test(char)) { // 1 // char是一个小写字母或者数字 currentToken.value += char; return jsxIdentifier; //返回的状态是继续收集标识符 } else if (char === " ") { //遇到空格,表示一个jsx收集完毕 emit(); //可能需要收集属性了 return attribute; } else if (char === ">") { emit(); // 这个jsx没有属性的 currentToken.type = types.rightParenthese; currentToken.value = ">"; emit(); return foundRightParenthese; } } // 准备收集属性的函数 function attribute(char) { // id="xx" if (LETTERs.test(char)) { currentToken.type = types.AttributeKey; currentToken.value += char; // 需要收集key了 return attributeKey; } } //准备收集属性key的函数 function attributeKey(char) { if (LETTERs.test(char)) { currentToken.value += char; return attributeKey; } else if (char === "=") { // key收集完毕 emit(); currentToken.type = types.Equal; currentToken.value = "="; emit(); return attributeValue; } } //准备收集属性的value了 function attributeValue(char) { // "xx" if (char === '"') { currentToken.type = types.AttributeStringVlaue; currentToken.value = ""; return AttributeStringVlaue; } } // 收集属性字符串value function AttributeStringVlaue(char) { // xx" if (LETTERs.test(char)) { currentToken.value += char; return AttributeStringVlaue; } else if (char === '"') { //收集结束 emit(); //继续收集下一个属性或者是 '>' return tryLeaveAttribute; } } // 试图离开属性收集 function tryLeaveAttribute(char) { if (char === " ") { //后面是空格,表明是一个新的属性 return attribute; } else if (char === ">") { //结束收集 currentToken.type = types.rightParenthese; currentToken.value = ">"; emit(); // 状态到了找到'>的状态 return foundRightParenthese; } else { throw new TypeError("不是合法的字符串"); } } // 遇到了>的状态 function foundRightParenthese(char) { if (char === "<") { //开启新的元素 return start(char); } else { //否则就是jsx文本 currentToken.value += char; currentToken.type = types.JsxText; return jsxText; } } // 遇到了jsxText后的状态 function jsxText(char) { if (char) { //结束jsxText收集 if (char === "<") { emit(); // 开启新元素的收集 currentToken.type = types.leftParenthese; currentToken.value = "<"; emit(); return foundLeftParenthese; } else { currentToken.value += char; return jsxText; } } else { currentToken.value += char; return jsxText; } } function tokenizer(input) { let state = start; //第一次处于start状态 for (let char of input) { //遍历所有字符串 if (state) { state = state(char); } } return tokens; } const code = `<h1 id="title"><span>hello</span>world</h1>`; console.log(tokenizer(code)); module.exports = { tokenizer, };
语法分析
- 语法分析的原理和递归下降算法(Recursive Descent Parsing)
- 并初步了解上下文无关文法(Context-free Grammar,CFG)
递归下降算法
- 它的左边是一个非终结符(Non-terminal)
- 右边是它的产生式(Production Rule)
- 在语法解析的过程中,左边会被右边替代。如果替代之后还有非终结符,那么继续这个替代过程,直到最后全部都是终结符(Terminal),也就是
Token
- 只有终结符才可以成为
AST
的叶子节点。这个过程,也叫做推导(Derivation)过程 - 上级文法嵌套下级文法,上级的算法调用下级的算法。表现在生成 AST 中,上级算法生成上级节点,下级算法生成下级节点。这就是
下降
的含义
上下文无关文法
- 上下文无关的意思是,无论在任何情况下,文法的推导规则都是一样的(优先级低的先推导,优先级高的后推导,付父规则先推导,子规则后推导)
- 规则分成两级:第一级是加法规则,第二级是乘法规则。把乘法规则作为加法规则的子规则
- 解析形成 AST 时,乘法节点就一定是加法节点的子节点,从而被优先计算
- 加法规则中还
递归
地又引用了加法规则
遍历
转换
生成
文章推荐
- https://zhuanlan.zhihu.com/p/465510312
- https://zhuanlan.zhihu.com/p/521267484
- https://www.cnblogs.com/wind-alex/p/10375236.html
- https://youle.zhipin.com/articles/e9f7f70342ca4defqxB73tu1FFE~.htm