编译原理是不是高深莫测?如果你直接翻开《龙书》,很可能被一堆符号劝退。
但别慌——今天我们通过一个“小实验”,用 JavaScript 从零写一个 四则运算解释器。
这个实验会带你完整走一遍编译流程:
- 定义四则运算(词法 & 语法)
- 词法分析(字符流 → Token 流)
- 语法分析(Token 流 → AST)
- 解释执行(遍历 AST,计算结果)
没错,我们要亲手造一个“迷你版编译器” 🚀。
1. 定义四则运算
四则运算包含:加、减、乘、除。比如:
1 + 2 * 3
我们要告诉编译器如何理解这些符号,这就需要:
-
词法定义(Token):
- Number → 数字(0~9 的组合)
- Operator →
+ - * / - Whitespace → 空格
- LineTerminator → 换行符
-
语法定义(BNF):
<Expression> ::= <AdditiveExpression><EOF> <AdditiveExpression> ::= <MultiplicativeExpression> | <AdditiveExpression> <+> <MultiplicativeExpression> | <AdditiveExpression> <-> <MultiplicativeExpression> <MultiplicativeExpression> ::= <Number> | <MultiplicativeExpression> <*> <Number> | <MultiplicativeExpression> </> <Number>
简单来说:
- 乘除优先,所以先定义 MultiplicativeExpression;
- 加减其次,由乘法表达式组合而来;
- Number 是最基本的单元。
2. 词法分析:状态机实现
我们先把输入的字符串(比如 "1024 + 2 * 256")切分成 Token 流。
let tokens = [];
let current = "";
function emitToken(type, value) {
tokens.push({ type, value });
}
const start = char => {
if (/[0-9]/.test(char)) {
current += char;
return inNumber;
}
if ("+-*/".includes(char)) {
emitToken(char, char);
return start;
}
if (char === " " || char === "\n" || char === "\r") {
return start; // 跳过空格换行
}
if (char === Symbol("EOF")) {
return;
}
};
const inNumber = char => {
if (/[0-9]/.test(char)) {
current += char;
return inNumber;
} else {
emitToken("Number", current);
current = "";
return start(char); // 回退一个字符
}
};
// 测试
let input = "1024 + 2 * 256";
let state = start;
for (let c of input.split("")) state = state(c);
state(Symbol("EOF"));
console.log(tokens);
输出结果:
[
{ type: 'Number', value: '1024' },
{ type: '+', value: '+' },
{ type: 'Number', value: '2' },
{ type: '*', value: '*' },
{ type: 'Number', value: '256' }
]
完美 🎉
3. 语法分析:构造 AST
接下来把 Token 流变成一棵 AST(抽象语法树)。
function MultiplicativeExpression(source) {
if (source[0].type === "Number") {
let node = { type: "MultiplicativeExpression", children: [source[0]] };
source[0] = node;
return MultiplicativeExpression(source);
}
if (
source[0].type === "MultiplicativeExpression" &&
source[1] &&
(source[1].type === "*" || source[1].type === "/")
) {
let node = {
type: "MultiplicativeExpression",
operator: source[1].type,
children: [source.shift(), source.shift(), source.shift()]
};
source.unshift(node);
return MultiplicativeExpression(source);
}
return source[0];
}
function AdditiveExpression(source) {
if (source[0].type === "MultiplicativeExpression") {
let node = { type: "AdditiveExpression", children: [source[0]] };
source[0] = node;
return AdditiveExpression(source);
}
if (
source[0].type === "AdditiveExpression" &&
source[1] &&
(source[1].type === "+" || source[1].type === "-")
) {
let node = {
type: "AdditiveExpression",
operator: source[1].type,
children: [source.shift(), source.shift(), MultiplicativeExpression(source)]
};
source.unshift(node);
return AdditiveExpression(source);
}
return source[0];
}
function Expression(source) {
AdditiveExpression(source);
if (source[0].type === "AdditiveExpression" && source[1] && source[1].type === "EOF") {
let node = { type: "Expression", children: [source.shift(), source.shift()] };
source.unshift(node);
return node;
}
throw new Error("Unexpected token sequence");
}
// 测试
let source = [
{ type: "Number", value: "3" },
{ type: "*", value: "*" },
{ type: "Number", value: "300" },
{ type: "+", value: "+" },
{ type: "Number", value: "2" },
{ type: "*", value: "*" },
{ type: "Number", value: "256" },
{ type: "EOF" }
];
let ast = Expression(source);
console.log(JSON.stringify(ast, null, 2));
输出一棵树,表示加法由乘法组成。
4. 解释执行:遍历 AST
最后一步,遍历 AST,计算结果。
function evaluate(node) {
if (node.type === "Expression") {
return evaluate(node.children[0]);
}
if (node.type === "AdditiveExpression") {
if (node.operator === "+") {
return evaluate(node.children[0]) + evaluate(node.children[2]);
}
if (node.operator === "-") {
return evaluate(node.children[0]) - evaluate(node.children[2]);
}
return evaluate(node.children[0]);
}
if (node.type === "MultiplicativeExpression") {
if (node.operator === "*") {
return evaluate(node.children[0]) * evaluate(node.children[2]);
}
if (node.operator === "/") {
return evaluate(node.children[0]) / evaluate(node.children[2]);
}
return evaluate(node.children[0]);
}
if (node.type === "Number") {
return Number(node.value);
}
}
// 测试执行
console.log(evaluate(ast)); // 812
是不是很酷?我们用不到 200 行 JS,就实现了一个解释器!
总结
通过这个实验,你已经完整走了一遍编译流程:
- 词法分析(字符 → Token)
- 语法分析(Token → AST)
- 解释执行(AST → 结果)
虽然这是一个简化版,但它揭示了 编译原理的核心思想。
🎯 进阶挑战
如果你想更进一步,可以尝试:
- ✅ 补全
emitToken逻辑,让词法分析更健壮 - ✅ 支持小数运算
- ✅ 引入负数解析
- ✅ 给解释器加上括号优先级
( )
这些挑战做完,你就能把这个解释器进化成一个 迷你计算器!