每个已经编程过的人可能已经同说过关于解析器,主要是遇到"parser error"错误。或者可能听到或者甚至说一些像“我们需要解析它”,“在它接着解析后”,“解析因为输入这个而崩溃”。单词"parser"和"compiler""interpreter"和"programming language"一样常见。每个人都知道parsers的存在,它们必须,对吗?因为其他人会对解析错误负责?
但是究竟什么是解析器?他的工作是什么并且它是怎样工作的?Wikipedia如是说:
解析器是一个软件组件,它获取输入数据(通常是文本)并构建数据结构——通常是某种解析树、抽象语法树或其他层次结构——给出输入的结构表示,在过程中检查正确的语法。 [...] 解析器之前通常有一个单独的词法分析器,它从输入字符序列中创建标记;
对于有关计算机科学主题的维基百科文章,此摘录非常容易理解。 我们甚至可以在那里看到我们的词法分析器!
解析器将其输入转换为表示输入的数据结构。这听起来很抽象,所以让我们用一个例子来说明这一点。这是一小段Javascript:
>varinput='{"name": "Thorsten", "age": 28}';
>varoutput=JSON.parse(input);
>output
{name:'Thorsten',age:28}
>output.name
'Thorsten'
>output.age
28
>
我们的输入只是一些文本,字符。我们然后通过它传递给隐藏在JSON.parse函数后面的解析器并接受输出值。这个输出是表示输入的数据结构:一个Javascript对象,有两个字段,name和age,它们的值也对应于输入。我们现在可以很容易地使用这个数据结构,如访问名称和年龄字段所示。
“但是”,我听到你说,“一个JSON解析器与一个编程语言的解析器不同!它们不一样!”我能看到你在哪里说出了这段话,但不,它们没什么不同。至少在概念层面上没什么不同。一个JSON解析器将文本作为输入并构建表示输入的数据结构。这正是编程语言的解析器所做的。不同之处在于,在JSON解析器的情况下,您可以在查看输入时看到数据结构,而如果你看这个
if ((5+2*3)==91) { return computeStuff(input1,input2);}
用数据结构表示这一点并不是很明显。这就是为什么,至少对我来说,它们在更深的概念层面上似乎不同。我的猜测是,这种概念差异的感知主要是对于编程语言解析器及其产生的数据结构缺乏熟悉。与比解析编程语言相比,我在编写JSON,使用解析器解析它并检查解析器的输出方面有更多的经验。作为编程语言的用户,我们很少看到解析的源代码及其内部表示或与之交互。Lisp程序员是规则的例外——在Lisp中,用于表示源代码的数据结构是Lisp用户使用的数据结构。解析后的源代码作为程序中的数据很容易访问。“代码就是数据,数据就是代码”是你经常从Lisp程序员那里听到的。
所以,为了将我们对编程语言解析器的概念理解提升到我们对序列化语言(如JSON、YAML、TOML、INI等)解析器的熟悉和直观水平,我们需要了解它们产生的数据结构。
在大多数解释器和编译器中,用于源代码内部表示的数据结构称为“语法树”或“抽象语法树(简称AST)”。抽象基于源代码中可见的某些细节是省略的。分号、换行符、空格、注释、大括号、方括号和圆括号——根据语言和解析器,这些细节不在 AST 中表示,而只是在构造它时指导解析器。
需要注意的是,没有一种真正的,通用的AST格式被每个解析器使用。它们的实现都非常相似,概念相同,但它们在细节上有所不同。具体实现取决于被解析的编程语言。
一个小例子应该可以让事情很简单,让我们假设我们已经有了以下源代码:
if (3 * 5 > 10) {
return "hello";
} else {
return "goodbye";
}
并且让我们使用Javascript,有一个MagicLexer,一个MagicParser并且AST是由Javascript对象构建的,那么解析步骤可能会产生这样的结果:
>varinput='if (3 * 5 > 10) { return "hello"; } else { return "goodbye"; }';
>vartokens=MagicLexer.parse(input);
>MagicParser.parse(tokens)
{
type:"if-statement",
condition:{
type:"operator-expression",
operator:">",
left:{
type:"operator-expression",
operator:"*",
left:{type:"integer-literal",value:3},
right:{type:"integer-literal",value:5}
},
right:{type:"integer-literal",value:10}
},
consequence:{
type:"return-statement",
returnValue:{type:"string-literal",value:"hello"}
},
alternative:{
type:"return-statement",
returnValue:{type:"string-literal",value:"goodbye"}
}
}
正如你所见,解析器的输出,AST,是非常抽象的:没有括号,没有分号,也没有大括号。但它非常准确地代表了源代码,你不觉得吗?我敢打赌您现在可以在回顾源代码时“看到”AST结构!
所以这就是解析器做的,它们吃掉源代码作为输入(还有文本或tokens)并且产生与源代码相映的数据结构。当建立数据结构,它们不可避免地分析输入,检查它是否符合预期的结构。因此,解析的过程也称为句法分析。
在本章节,我们将写下我们的Monkey语言的解析器。它的输入将会是我们上一个章节定义的tokens,生产通过我们已经写的词法分析器。我们将定义我们自己的AST,定制我们Monkey编程语言的需求,并在递归解析tokens的同时构造此AST实例。
< 1.5开始一个REPL | > 2.2为什么不是解析器生成器? |
---|