【动手写ToyLang】1.从四则运算表达式开始
# 由此开始
如果你曾经接触过《编译原理》的话,不知道是否与我有着同样的困惑呢?
各种不近人情的名词,公式乱飞,这让我学习起来格外痛苦。
我在阅读过不少文章、书籍,并且尝试敲下一些代码之后,才逐渐理解了一些较为关键的东西。
实际上,当我真正完成了对四则运算表达式的解析的那一刻,我才真切感受到了编译原理的优雅与美妙之处,这大概就是由人类智慧的伟大之处吧 (偏得有点远了)。
因此,我才会选择先从四则运算表达式开始,将其逐步扩展成为一门 通用编程语言
,也能让读者每一节都能感受到学习有所反馈的喜悦。
# 初尝构思
现在,我来尝试给你出一道题吧,请你用你所熟悉的语言,编写一个模块:
- 输入符合四则运算表达式规范的字符串 (可以假定只有整数,不存在括号);
- 输出整型结果;
- 要求关键逻辑由自己实现,不可借由库、语言本身提供的功能。
你能实现吗?
是否觉得脑子有些空白,难以组织成较为优雅的实现思路呢?
如果你现在就能想到很棒的解法,那至少你要比我厉害多了。
我曾经因为某些需求做过尝试,虽然最后写出来了,但是具体实现也非常丑陋,这里就不献丑了。
但是我可以给你大致描述一下我当初的思路:
-
首先,查找字符串中优先级较高的运算符
*
/
,再前 / 后瞻运算符前后的数字,这个子串也能形成一个表达式; -
算出结果后,在原表达式中,用结果替换掉子串 (刚刚被计算的表达式),再继续查找。
-
完成后再从头开始查找优先级更低一级的运算符
+
-
,直到运算完成。
当然,我们不会用这么低效的方法,也不会要求现在就给出实现。
现在提出这个问题,并不是为了为难你,只是为了让你记住眼下的感觉,当你学完本系列文章后,再回来看看,这一道题,是否真的有那么难呢?
# 抽象语法树
咱们先来看百度百科对 抽象语法树
的定义:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
嗯… 不愧是名字就带着抽象的东西,连说明都这么抽象。
咱们先不整那么多虚头巴脑的,干就完了。
# 构造四则运算表达式的 AST
3 + 2
这个是大家熟悉的算术表达式,实际上,我们的大脑是如何计算这个表达式的呢?当然需要遵循某些规则:
- 先乘除
- 再加减
- 从左往右结合
那我们自然是需要先左往右找在表达式中找到 *
、 /
两种符号,好消息是,没有,嘿嘿。
接下来继续左往右找 +
、 -
,找到之后将其取出,这也是我们需要计算的子表达式。
我们将其分成三部分,分别是:
- 左边的数字
3
- 加法运算符
+
- 右边的数字
2
人脑的表达式计算模型,可以归纳成这么一颗树型结构:
遍历树与我们的计算过程极度相似:
- 根节点,是加法,表明需要将左子节点与右子节点相加;
3 + 2
,返回结果5
;- 遍历结束;
# 多运算符与结合律
那么,当一个表达式中,存在多个运算符时,应该如何构造语法树呢?
接下来我们构造表达式 3 - 2 + 4
的树。
首先我们应该注意的一点是,需要先被执行运算 (优先级更高) 的子表达式,其节点的相对深度更深:
还是先来遍历这颗树:
- 根节点,是加法,表明需要将左子节点与右子节点相加;
- 左子节点是减法,表明需要用左子节点减去右子节点,需要继续向下展开;
3 - 2
,返回结果1
;
1 + 4
,返回结果5
;- 遍历结束;
因此,遍历的过程也符合我们所要求的 左结合律
。
- 由左边的运算符开始,向右结合。
- 同优先级下,我们需要优先计算左边的运算符组成的子表达式,
接下来我再给你画一下另一种情况,你也就明白了。
你可以尝试一下遍历这棵树,会发现先被执行的一定是加法,这并不符合我们的从一开始就要求的 左结合律
。
在部分情况下,由于结合律的错误,就会产出错误的结果:
- 如我们本次求解的表达式,通过遍历此树得出的结果为
-3
。
# 目标:根据语法规则生成 AST
我不说你大概也已经猜到了,其实我们上面所构造的树,在当前的应用场景下,就叫做 抽象语法树
。
遍历抽象语法树并计算的过程,与我们人脑对表达式的计算的过程是十分相似的。
也就是说,我们只需要构造出这么一颗抽象语法树,就已经基本上完成了编译工作的一大半。
此时,若需要执行编译产生的结果,只需要遍历我们所生成 AST 即可。
如下图:
在遍历的过程中,我们就能够正确的完成对加法表达式的加法运算,再完成对赋值表达式的赋值运算。
# AST 解释器
最后,笔者向读者展示了有关 "解释器" 的东西,是的,即便你难以置信。
遍历这棵树的过程,就可以叫做解释;
如果我们写出代码实现遍历树的过程,即是 AST解释器
。
当然,解释器是一种比较广泛的概念,即便到后面我们基于 AST 生成了字节码,交给虚拟机执行,我们的程序也依然可以称之为解释器,只不过解释的对象从 AST 换成了字节码。
我个人觉得,当初造这个词的人只是为了区分所谓的 "编译型语言" 和 "解释型语言"。
以下是百度百科对解释器的定义:
解释器(英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位 “中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。
- 此处关于
每转译一行程序叙述就立刻运行
的说明也不尽然,至少现在很多被称为解释器
的程序,并非如此 (如 Python 解释器、Java 解释器等)。