重学前端(12)理解编译原理:一个四则运算的解释器
帮助我们快速理解编译原理相关的知识。
分析按照编译原理相关的知识,我们来设计一下工作,这里我们分成几个步骤。
- 定义四则运算:产出四则运算的词法定义和语法定义。
- 词法分析:把输入的字符串流变成 token。
- 语法分析:把 token 变成抽象语法树 AST。
- 解释执行:后序遍历 AST,执行得出结果。
定义四则运算
四则运算就是加减乘除四种运算,例如:1 + 2 * 3
首先我们来定义词法,四则运算里面只有数字和运算符,所以定义很简单,但是我们还要注意空格和换行符,所以词法定义大概是下面这样的。
- Token
- Number: 1 2 3 4 5 6 7 8 9 0 的组合
- Operator: + 、-、 *、 / 之一
- Whitespace: <sp>
- LineTerminator:<LF> <CR>
这里我们对空白和换行符没有任何的处理,所以词法分析阶段会直接丢弃。
接下来我们来定义语法,语法定义多数采用 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>