CategoryResourceRepost/极客时间专栏/重学前端/模块一:JavaScript/(小实验)理解编译原理:一个四则运算的解释器.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

12 KiB
Raw Blame History

你好我是winter。

在前面的课程中我在JavaScript和CSS的部分多次提到了编译原理相关的知识。这一部分的知识如果我们从编译原理“龙书”等正规的资料中学习就会耗费掉不少的时间所以我在这里设计了一个小实验帮助你快速理解编译原理相关的知识。

今天的内容比较特殊,我们来做一段详细的代码实验,详细的代码我放在了文章里,如果你正在收听音频,可以点击文章查看详情。

分析

按照编译原理相关的知识,我们来设计一下工作,这里我们分成几个步骤。

  • 定义四则运算:产出四则运算的词法定义和语法定义。
  • 词法分析把输入的字符串流变成token。
  • 语法分析把token变成抽象语法树AST。
  • 解释执行后序遍历AST执行得出结果。

定义四则运算

四则运算就是加减乘除四种运算,例如:

1 + 2 * 3

首先我们来定义词法,四则运算里面只有数字和运算符,所以定义很简单,但是我们还要注意空格和换行符,所以词法定义大概是下面这样的。

  • Token
      - Number: `1` `2` `3` `4` `5` `6` `7` `8` `9` `0` 的组合 - Operator: `+` 、`-`、 `*`、 `/` 之一

      这里我们对空白和换行符没有任何的处理,所以词法分析阶段会直接丢弃。

      接下来我们来定义语法语法定义多数采用BNF但是其实大家写起来都是乱写的比如JavaScript标准里面就是一种跟BNF类似的自创语法。

      不过语法定义的核心思想不会变,都是几种结构的组合产生一个新的结构,所以语法定义也叫语法产生式。

      因为加减乘除有优先级,所以我们可以认为加法是由若干个乘法再由加号或者减号连接成的:

      <Expression> ::= 
          <AdditiveExpression><EOF>
      
      <AdditiveExpression> ::= 
          <MultiplicativeExpression>
          |<AdditiveExpression><+><MultiplicativeExpression>
          |<AdditiveExpression><-><MultiplicativeExpression>
      
      

      这种BNF的写法类似递归的原理你可以理解一下它表示一个列表。为了方便我们把普通数字也得当成乘法的一种特例了。

      <MultiplicativeExpression> ::= 
          <Number>
          |<MultiplicativeExpression><*><Number>
          |<MultiplicativeExpression></><Number>
      
      

      好了,这就是四则运算的定义了。

      词法分析:状态机

      词法分析部分我们把字符流变成token流。词法分析有两种方案一种是状态机一种是正则表达式它们是等效的选择你喜欢的就好这里我都会你介绍一下状态机。

      根据分析我们可能产生四种输入元素其中只有两种token我们状态机的第一个状态就是根据第一个输入字符来判断进入了哪种状态

      var token = [];
      const start = char => {
          if(char === '1' 
              || char === '2'
              || char === '3'
              || char === '4'
              || char === '5'
              || char === '6'
              || char === '7'
              || char === '8'
              || char === '9'
              || char === '0'
          ) {
              token.push(char);
              return inNumber;   
          }
          if(char === '+' 
              || char === '-'
              || char === '*'
              || char === '/'
          ) {
              emmitToken(char, char);
              return start
          }
          if(char === ' ') {
              return start;
          }
          if(char === '\r' 
              || char === '\n'
          ) {
              return start;
          }
      }
      const inNumber = char => {
          if(char === '1' 
              || char === '2'
              || char === '3'
              || char === '4'
              || char === '5'
              || char === '6'
              || char === '7'
              || char === '8'
              || char === '9'
              || char === '0'
          ) {
              token.push(char);
              return inNumber;
          } else {
              emmitToken("Number", token.join(""));
              token = [];
              return start(char); // put back char
          }
      }
      
      

      这个状态机非常简单它只有两个状态因为我们只有Number不是单字符的token。

      这里我的状态机实现是非常经典的方式用函数表示状态用if表示状态的迁移关系用return值表示下一个状态。

      下面我们来运行一下这个状态机试试看:

      
      function emmitToken(type, value) {
          console.log(value);
      }
      
      var input = "1024 + 2 * 256"
      
      var state = start;
      
      for(var c of input.split(''))
          state = state(c);
      
      state(Symbol('EOF'))
      
      
      

      运行后我们发现输出如下:

      1024
      +
      2
      *
      256
      
      

      这是我们想要的答案。

      语法分析LL

      做完了词法分析我们开始进行语法分析LL语法分析根据每一个产生式来写一个函数首先我们来写好函数名

      function AdditiveExpression( ){
      
      
      }
      function MultiplicativeExpression(){
          
      
      }
      
      

      为了便于理解,我们就不做流式处理了,实际上一般编译代码都应该支持流式处理。

      所以我们假设token已经都拿到了

      var tokens = [{
          type:"Number",
          value: "1024"
      }, {
          type:"+"
          value: "+"
      }, {
          type:"Number",
          value: "2"
      }, {
          type:"*"
          value: "*"
      }, {
          type:"Number",
          value: "256"
      }, {
          type:"EOF"
      }];
      
      

      每个产生式对应着一个函数例如根据产生式我们的AdditiveExpression需要处理三种情况

      <AdditiveExpression> ::= 
          <MultiplicativeExpression>
          |<AdditiveExpression><+><MultiplicativeExpression>
          |<AdditiveExpression><-><MultiplicativeExpression>
      
      

      那么AddititveExpression中就要写三个if分支来处理三种情况。

      AdditiveExpression的写法是根传入的节点利用产生式合成新的节点

      function AdditiveExpression(source){
          if(source[0].type === "MultiplicativeExpression") {
              let node = {
                  type:"AdditiveExpression",
                  children:[source[0]]
              }
              source[0] = node;
              return node;
          } 
          if(source[0].type === "AdditiveExpression" && source[1].type === "+") {
              let node = {
                  type:"AdditiveExpression",
                  operator:"+",
                  children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
              }
              source.unshift(node);
          }
          if(source[0].type === "AdditiveExpression" && source[1].type === "-") {
              let node = {
                  type:"AdditiveExpression",
                  operator:"-",
                  children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
              }
              source.unshift(node);
          }
      }
      
      

      那么下一步我们就把解析好的token传给我们的顶层处理函数Expression。

      Expression(tokens);
      
      

      接下来我们看Expression该怎么处理它。

      我们Expression收到第一个token是个Number这个时候Expression就傻了这是因为产生式只告诉我们收到了 AdditiveExpression 怎么办。

      这个时候我们就需要对产生式的首项层层展开根据所有可能性调用相应的处理函数这个过程在编译原理中称为求“closure”。

      function Expression(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;
          }
          AdditiveExpression(source);
          return Expression(source);
      }
      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 === "+") {
              let node = {
                  type:"AdditiveExpression",
                  operator:"+",
                  children:[]
              }
              node.children.push(source.shift());
              node.children.push(source.shift());
              MultiplicativeExpression(source);
              node.children.push(source.shift());
              source.unshift(node);
              return AdditiveExpression(source);
          }
          if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "-") {
              let node = {
                  type:"AdditiveExpression",
                  operator:"-",
                  children:[]
              }
              node.children.push(source.shift());
              node.children.push(source.shift());
              MultiplicativeExpression(source);
              node.children.push(source.shift());
              source.unshift(node);
              return AdditiveExpression(source);
          }
          if(source[0].type === "AdditiveExpression")
              return source[0];
          MultiplicativeExpression(source);
          return AdditiveExpression(source);
      }
      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 === "*") {
              let node = {
                  type:"MultiplicativeExpression",
                  operator:"*",
                  children:[]
              }
              node.children.push(source.shift());
              node.children.push(source.shift());
              node.children.push(source.shift());
              source.unshift(node);
              return MultiplicativeExpression(source);
          }
          if(source[0].type === "MultiplicativeExpression"&& source[1] && source[1].type === "/") {
              let node = {
                  type:"MultiplicativeExpression",
                  operator:"/",
                  children:[]
              }
              node.children.push(source.shift());
              node.children.push(source.shift());
              node.children.push(source.shift());
              source.unshift(node);
              return MultiplicativeExpression(source);
          }
          if(source[0].type === "MultiplicativeExpression")
              return source[0];
      
          return MultiplicativeExpression(source);
      };
      
      var 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"
      }];
      var ast = Expression(source);
      
      console.log(ast);
      
      

      解释执行

      得到了AST之后最困难的一步我们已经解决了。这里我们就不对这颗树做任何的优化和精简了那么接下来直接进入执行阶段。我们只需要对这个树做遍历操作执行即可。

      我们根据不同的节点类型和其它信息写if分别处理即可

      
      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);
          }
      }
      
      

      总结

      在这个小实验中我们通过一个小实验学习了编译原理的基本知识小实验的目的是帮助你理解JavaScript课程中涉及到的编译原理基本概念它离真正的编译原理学习还有很大的差距。

      通过实验,我们了解了产生式、词法分析、语法分析和解释执行的过程。

      最后留给你一些挑战,你可以根据自己的水平选择:

      • 补全emmitToken使得我们的代码能完整工作起来。
      • 为四则运算加入小数。
      • 引入负数。
      • 添加括号功能。

      欢迎写好的同学留言给我。