10、(小实验)理解编译原理:写一个四则运算解释器

86 阅读3分钟

编译原理是不是高深莫测?如果你直接翻开《龙书》,很可能被一堆符号劝退。
但别慌——今天我们通过一个“小实验”,用 JavaScript 从零写一个 四则运算解释器

这个实验会带你完整走一遍编译流程:

  1. 定义四则运算(词法 & 语法)
  2. 词法分析(字符流 → Token 流)
  3. 语法分析(Token 流 → AST)
  4. 解释执行(遍历 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 逻辑,让词法分析更健壮
  • ✅ 支持小数运算
  • ✅ 引入负数解析
  • ✅ 给解释器加上括号优先级 ( )

这些挑战做完,你就能把这个解释器进化成一个 迷你计算器