所有的编译器,都逃不出这5个阶段:
源代码
↓
词法分析(Lexical Analysis)
↓
语法分析(Syntax Analysis)
↓
语义分析(Semantic Analysis)
↓
中间表示(IR)
↓
代码生成 / 转换
第一个阶段:词法分析
把“字符流”变成“token流”。有点类似于给某些字符打上标签,分门别类。
一般会使用 正则匹配、状态机 两种方式进行词法分析。
正则匹配:但是正则匹配在编译器场景下,不太合适,因为想要大量的正则表示式并且有冲突,同时不易维护,性能不高。
状态机:有穷状态自动机(FSM),在有限个输入的情况下,在这些状态中转移并期望最终达到终止状态。
// 字符
c o n s t a = 1 ;
// token
[const] [identifier:a] [=] [number:1] [;]
第二个阶段:语法分析
语法分析的目的是通过词法分析拿到的token流,结合文法规则,通过一定算法得到一颗抽象语法树(AST)。这一个步骤中的基础是文法。
那么什么是文法呢?
编译器需要通过什么规则生成。这是一套规则。通常使用的是上下文无关文法(CFG)。
从生成AST效率和实现难度上,总结有两种解析算法:
- 自顶而下,简单、递归下降
- 自底而上就,强大、复杂
抽象语法树(AST)
树结构表示代码的逻辑结构。
[const] [a] [=] [1]
VariableDeclaration
├─ kind: const
└─ Declarator
├─ Identifier(a)
└─ Literal(1)
第三阶段:语义分析
让结构正确的代码变得有意义。主要对作用域、类型、符号表进行分析。语法正确并不表示语义正确。
let a = 1;
let a = 2; // 语法 OK,语义错误
第四阶段:中间表示(IR)
介于源语言和目标语言之间的统一表示,这一个过程是实现跨平台,统一优化逻辑的关键步骤。Babel的AST本质上就是IR
第五阶段:代码生成
把AST/IR输出成目标语言。
总流程
input => tokenizer => tokens; // 词法分析
tokens => parser => ast; // 语法分析、语义分析
ast => transformer => newAst; //中间表示
newAst => generator => output; //代码生成
公式编译执行器
// Tokenizer
function tokenizer(expression) {
const tokens = [];
let current = 0;
while (current < expression.length) {
let char = expression[current];
if (/\d/.test(char)) {
let number = "";
while (/\d/.test(char)) {
number += char;
char = expression[++current];
}
tokens.push({ type: "number", value: number });
continue;
}
if (/[a-zA-Z]/.test(char)) {
let id = "";
while (/[a-zA-Z]/.test(char)) {
id += char;
char = expression[++current];
}
while (expression[current] === " ") {
current++;
}
if (expression[current] === "(") {
tokens.push({ type: "function", value: id });
} else {
tokens.push({ type: "id", value: id });
}
continue;
}
if (char === "(" || char === ")" || char === ",") {
tokens.push({ type: char, value: char });
current++;
continue;
}
if (char === ".") {
tokens.push({ type: "dot", value: "." });
current++;
continue;
}
if (char === " ") {
current++;
continue;
}
throw new TypeError(`Unknown character: ${char}`);
}
return tokens;
}
// Parser
function parse(tokens) {
let current = 0;
function walk() {
let token = tokens[current];
if (token.type === "number") {
current++;
return { type: "NumberLiteral", value: token.value };
}
if (token.type === "function") {
current++;
const node = {
type: "CallExpression",
name: token.value,
params: [],
};
token = tokens[++current];
while (token.type !== ")") {
node.params.push(walk());
token = tokens[current];
if (token.type === ",") {
current++;
}
}
current++;
return node;
}
if (token.type === "id") {
let value = token.value;
current++;
while (tokens[current] && tokens[current].type === "dot") {
current++;
if (tokens[current] && tokens[current].type === "id") {
value += "." + tokens[current].value;
current++;
} else {
throw new TypeError("Expect a variable after '.'");
}
}
return { type: "Variable", value };
}
throw new TypeError(`Unknown token type: ${token.type}`);
}
let ast = { type: "Program", body: [] };
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
// Interpreter
function interpreter(ast, context = {}) {
const operators = {
Add: (a, b) => a + b,
Subtract: (a, b) => a - b,
Multiply: (a, b) => a * b,
Divide: (a, b) => a / b,
};
function traverseNode(node) {
if (node.type === "NumberLiteral") {
return Number(node.value);
}
if (node.type === "Variable") {
const keys = node.value.split(".");
let value = context;
for (let key of keys) {
if (!(key in value)) {
if (keys.indexOf(key) === keys.length - 1) {
console.log(value, "...", key);
throw new TypeError(`Unknown variable: ${node.value}`);
} else {
continue;
}
}
value = value[key];
}
return value;
}
if (node.type === "CallExpression") {
const args = node.params.map(traverseNode);
const func = operators[node.name];
if (!func) {
throw new TypeError(`Unknown function: ${node.name}`);
}
return func.apply(null, args);
}
throw new TypeError(`Unknown node type: ${node.type}`);
}
return traverseNode(ast.body[0]);
}
const person = { age: 2 };
const expression =
"Subtract(Add(3, Multiply(4, person.age)), Divide(6, person.age))";
const tokens = tokenizer(expression);
console.log(tokens);
const ast = parse(tokens);
const result = interpreter(ast, person);
console.log(result);