我目前正参与我们的一个大项目,Alloy。Alloy 是一种编译型的编程语言。我目前在计算机及编程领域最喜欢的一个爱好就是语言。事实上,我认为每个程序员都应该对编程语言是如何工作的有个基本的了解,这就是我写这个系列的原因。
这是系列文章中的第一篇文章。该系列将描述我已经写过的代码,来向你展示如何制作自己的编程语言。这里注意一下,本文假设你对编译器/解释器的理论/实践有已有很少或没有过往经验。还有要注意的是,这一系列的文章不是介绍编程或Go编程的。
什么是解释器(interpreter)?解释器会直接执行或表现写在某特定脚本语言中的指令。这可以是一种已存在的脚本语言,像 Python或者 Ruby。它也可以是一种你自己创造的脚本语言,这将是我们在这里要做的。这一系列将从Go的基础开始指导你实现自己的脚本语言/解释器“玩具”。
为什么是“玩具”脚本语言/解释器?解释器可以是极其复杂的。现代解释器(比如Ruby或Python)十分庞大,包括成百上千行,甚至多达百万的代码量。这对一个新手来说不太容易理解。玩具语言是个更为简化的版本,它们常常跳过或者省去一些短语(在这里我们将不考虑优化)。制造一种玩具语言是一个理解它们如何工作的有效方法,当开始使用它们时,它们将实实在在地帮助你理解,即使你不是在一个已经存在的解释器(如Rust)上工作。
编程语言
你可以用任意一种你喜欢的语言构建一个解释器。在这个案例中,我将使用Go。这之前我还没写过许多Go,所以对我来说这也是一个学习的经历!然而如果你不习惯用Go写,你可以用如下任一种语言制作你的解释器,可以是 C,Java,或者甚至是 JavaScript。
小结由于在当今世界有如此多的解释器和编译器,因此有许多工具可以来帮助你制作它们。你需要决定是否考虑偷偷使用一个外部工具,或者你想要自己写所有的代码。我更喜欢后者,因为我觉得如果我使用某个外部工具来代劳,我就学不会它如何工作。不过这完全取决于你自己。在解释器环境中,你是否使用这些工具会在编译器/解释器社区引起非常强烈的争论。一些人会告诉你如果你不用ANTLR,BISON或者其它一些工具那么你会出错。另一些人会说完成它的唯一方法就是亲手写你自己的词汇分析器(lexer)和语法分析器(parser)。最后,这是你的选择,但在这一系列文章中,我会至少会涵盖如何构建词汇分析器(Lexer)和语法分析器(Parser)。
理论
在深入之前,我们需要讲解一下理论。
什么是词汇分析器和语法分析器如果你看到这一段落,并困惑于我所指的词汇分析器和语法分析器,那么不用担心。典型做法是把这个分析过程分成不同的阶段。有些阶段是可选的,换句话叫做优化阶段。但是大部分现代解析器几乎处理所有阶段。让我们深入去看看这些阶段吧。
词法分析第一阶段是语法分析,基本上就是一个分词器。词法分析、解析器或者语法分析把字符或者输入流分割成标记。这些标记以列表或者容器等数据结构存成标记流。解析器通过归类这些词(输入流中的符号字符串),给于特定的标记某种含义。例如,*,=,+等词可以归为操作符,tost 和bacon可以归为字符串常亮,而’a‘和’b‘则是字符。
解析
解析器是一个翻译组件,它用来接收数据的输入,一个词法分析器产生令牌列表,并产生一个表达式,通常是一种抽象的语法树,或其他结构。解释器遵循的规则被叫做语法,它是你定义的一种语言的方式,这些语法诸如Extended Backus- Naur Form (EBNF)和 BNF (Backus-Naur Form),它们被用来描述一种语言的语法。下面是一个被写成EBNF语法的例子:
letter = "A" | "a" | ... "Z" | "z" | "_"; digit = { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" }; identifier = letter { letter | digit };这可能对你没有任何意义。你可能认识这些在编程语言中的符号,例如管道|,花括号{}。所有的符号都有特殊的含义:
{ } - denotes repetition | - denotes an option, similar to OR [...] - optional terminal/nonterminal ; - termination = - definition ... - sequence "..." - terminal string