原文地址:Let’s Build A Simple Interpreter. Part 3.
今天早上我醒来时想到:“为什么学习一项新技能这么困难呢?”
我不认为只是因为需要努力工作。我认为原因之一可能是我们花费了很多时间和精力通过阅 读和观察来获取知识,但没有花费足够的时间来通过练习把知识转化为技能。以游泳为例, 你可以花费很多时间阅读上百本关于游泳的书,跟有经验的游泳者和教练谈上数小时,观看 所有能找到的训练视频,但你第一次跃进泳池时仍然会像块石头一样沉下去。
关键是:你认为你对这个问题有多了解并不重要 ── 你必须把这些练习付诸实践才能把它变 成一项技能。为了帮助你练习,我在本系列的第一部分和第二部分中加入了练习。是的,你 在今天还有将来的文章中还会看到更多的练习,我保证:)
好了,让我们开始今天的学习,好吗?
目前为止,你已经学习了如何解释整数相加或相减的算术表达式如“7+3”或“12-9”。今天我 会聊一聊怎样解析(识别)并解释包含多位整数的加减法的算术表达式,如“7 - 3 + 2 - 1”。
本文中的算术表达式可以用如下的句法图(syntax diagram)表示:
Figure 1: syntax diagram
什么是句法图? 句法图 就是程序语言句法规则的图形表示。基本上,句法图从视觉上向 你展示了在你的程序语言中哪些语句是允许的哪些是不允许的。
句法图很容易阅读:只需跟随箭头所指示的路径即可。一些路径表示选择,一些路径表示循环。
你可以这样阅读上面的句法图:一个 term 后面可以跟一个加号或减号,后面又跟另一个 term, 相应地它后面又可以跟一个加号或减号,后面又跟另一个 term,如此循环。你已经读懂了 这幅图片,真的。你可能会疑惑什么是“term”。在这篇文章中“term”就是一个整数。
句法图主要有两个用途:
- 从图形上表示一个编程语言的标准(语法)。
- 用来帮助你编写 parser - 你可以通过下面简单的规则将图映射到代码。
你已经学过了从 token 流中识别组合的过程叫 parsing. 且解释器或编译器中执行这部 分任务的部分叫 parser. parsing 也被称为 句法分析 (syntax analysis),parser 也相应地被称为 句法分析器 (syntax analyzer),你应该也猜到这点了。
根据上面的句法图,下面所有的算术表达式都是合法的:
- 3
- 3 + 4
- 7 - 3 + 2 - 1
因为在不同的程序语言中算术表达式的句法规则都相似,我们可以使用 Python shell 来 “测试”我们的句法图。启动 Python shell 自己试一下:
>>> 3 3 >>> 3 + 4 7 >>> 7 - 3 + 2 - 1 5
一切正常。
但表达式“3+”就不是合法的算术表达式,因为根据句法图加号后面必须跟一个 term(整数), 否则就是句法错误。两次启动 Python shell 自己查看结果:
>>> 3 + File "<stdin>", line 1 3 + ^ SyntaxError: invalid syntax
使用 Python shell 来做测试是挺不错的,不过我们还是把上面的句法图映射到代码,用我 们自己的解释器来测试,是吧?
从前面的文章(第一部分和第二部分)你知道了 parser 和 interpreter 都在 expr
方
法中。再重复一下,parser 只是识别出结构并保证它符合某些规范,interpreter 在
parser 成功识别后对表达式进行求值。
下面的代码片段展示了与句法图相对应的 parser 的代码。句法图中的矩形盒子变成了解析
一个整数的 term
方法, expr
方法则只是跟随了句法图的指示:
1 | def term(self): |
可以看到 expr
方法首先调用了 term
方法。然后是一个可能执行 0 或多次的循环。
在循环中,parser 根据 token (是加号还是减号)来做选择。花些时间确认上面的代码确
实体现了算术表达式的句法图。
Parser 本身并不解释任何事:如果识别到一个表达式它就沉默,否则就抛出一个句法错误。
让我们修改 expr
方法来添加 interpreter 代码:
1 | def term(self): |
因为 interpreter 需要对表达式进行求值,所以 term
方法被修改为返回一个整数值,
expr
方法被修改为在适当的地方执行加减法并返回解释的结果。尽管代码很直接,但我
还是建议你花点时间研究它。
让我们继续前进,来看一下现在解释器的完整代码怎么样?
下面是你新版计算器的源代码,它可以处理包含任意多个整数的加减操作的合法算术表达式:
1 | # Token types |
将以上代码保存到名为 calc3.py
中,或者直接从 GitHub 上下载。试一试。亲眼看一下
它可以处理之前展示给你的句法图中包含的算术表达式规则。
下面是在我笔记本上的一次尝试:
$ python calc3.py calc> 3 3 calc> 7 - 4 3 calc> 10 + 5 15 calc> 7 - 3 + 2 - 1 5 calc> 10 + 1 + 2 - 3 + 4 + 6 - 15 5 calc> 3 + Traceback (most recent call last): File "calc3.py", line 147, in <module> main() File "calc3.py", line 142, in main result = interpreter.expr() File "calc3.py", line 123, in expr result = result + self.term() File "calc3.py", line 110, in term self.eat(INTEGER) File "calc3.py", line 105, in eat self.error() File "calc3.py", line 45, in error raise Exception('Invalid syntax') Exception: Invalid syntax
记得我在开头提到的练习吧:在这儿,说到做到:)
Figure 2: exercises
- 画一张只包含乘除法的算术表达式句法图,例如“7 * 4 / 2 * 3”。不开玩笑,拿只钢笔 或铅笔试试。
- 修改计算器的源代码使它解释只包含乘除法的算术表达式,如“7 * 4 / 2 * 3”。
- 从头写一个可以处理如“7 - 3 + 2 - 1”这样算术表达式的解释器。使用任何你喜欢的语 言都可以,只靠自己,不要参考例子。做这件事时,想想都需要包含的组件:lexer 获取 输入并把它转化为 token 流,parser 从 lexer 提供的 token 流中识别结构, interpreter 在 parser 成功识别到一个合法的算术表达式之后求得其结果。把这些连起 来。花点时间把你学到的知识转化为一个可以运行的算术表达式解释器。
检查你的理解。
- 什么是句法图?
- 什么是句法分析?
- 什么是句法分析器?
看呐!你读完了整篇文章。谢谢你今天在这浏览,别忘了做练习。:) 等我下次回来就会有 一篇新文章了 ── 敬请关注。
下面是我推荐的帮助你学习解释器和编译器的一些书:
- Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
- Writing Compilers and Interpreters: A Software Engineering Approach
- Modern Compiler Implementation in Java
- Modern Compiler Design
- Compilers: Principles, Techniques, and Tools (2nd Edition)
顺便说一下,我还在写一本书“et’s Build A Web Server: First Steps”,解释了怎么从头 写一个基本的 Web 服务器。你可以从这里, 这里 和 这里 来感受一下这本书。
本系列的所有文章:
- Let’s Build A Simple Interpreter. Part 1.
- Let’s Build A Simple Interpreter. Part 2.
- Let’s Build A Simple Interpreter. Part 3.
- Let’s Build A Simple Interpreter. Part 4.
- Let’s Build A Simple Interpreter. Part 5.
- Let’s Build A Simple Interpreter. Part 6.
- Let’s Build A Simple Interpreter. Part 7.
- Let’s Build A Simple Interpreter. Part 8.
- Let’s Build A Simple Interpreter. Part 9.
- Let’s Build A Simple Interpreter. Part 10.
- Let’s Build A Simple Interpreter. Part 11.
- Let’s Build A Simple Interpreter. Part 12.
- Let’s Build A Simple Interpreter. Part 13.
- Let’s Build A Simple Interpreter. Part 14.