如何编写简单的parser(基础篇)

51次阅读

共计 3338 个字符,预计需要花费 9 分钟才能阅读完成。

什么是 parser?
简单的说,parser 的工作即是将代码片段转换成计算机可读的数据结构的过程。这个“计算机可读的数据结构”更专业的说法是“抽象语法树(abstract syntax tree)”,简称 AST。AST 是代码片段具体语义的抽象表达,它不包含该段代码的所有细节,比如缩进、换行这些细节,所以,我们可以使用 parser 转换出 AST,却不能使用 AST 还原出“原”代码,当然,可以还原出语义一致的代码,就如同将 ES6 语法的 js 代码转换成 ES5 的代码。
parser 的结构
一般来说,一个 parser 会由两部分组成:

词法解析器(lexer/scanner/tokenizer)
对应语法的解释器(parser)

在解释某段代码的时候,先由词法解释器将代码段转化成一个一个的词组流(token),再交由解释器对词组流进行语法解释,转化为对应语法的抽象解释,即是 AST 了。
为了让大家更清楚的理解 parser 两部分的工作顺序,我们通过一个例子来进行说明:
437 + 734
在 parser 解析如上的计算表达式时,词法解析器首先依次扫描到“4”、“3”、“7”直到一个空白符,这时,词法解析器便将之前扫描到的数字组成一个类型为“NUM”的词组(token);接下来,词法解析器继续向下扫描,扫描到了一个“+”,对应输出一个类型为“PLUS”的词组(token);最后,扫描“7”、“3”、“4”输出另一个类型为“NUM”的词组(token)。语法解释器在拿到词法解析器输出的词组流后,根据词组流的“NUM”,“PLUS”,“NUM”的排列顺序,解析成为加法表达式。
由上的例子我们可以看出,词法解析器根据一定的规则对字符串进行解析并输出为词组(token),具体表现为连续不断的数字组合(“4”、“3”、“7”和“7”、“3”、“4”)即代表了数字类型的词组;语法解释器同样根据一定的规则对词组的组合进行解析,并输出对应的表达式或语句。在这里,词法解析器应用的规则即为词汇语法(Lexical Grammar)的定义,语法解释器应用的规则即为表达式(Expressions)、语句(Statements)、声明(Declarations)和函数(Functions)等的定义。
ECMAScript 标准
看到这里大家可能会感觉到奇怪,为什么讲 parser 讲的好好的,又跑到 ECMAScript 标准上来了呢?因为以上提到的词汇语法(Lexical Grammar)、表达式(Expressions)、语句(Statements)、声明(Declarations)和函数(Functions)等都是 ECMAScript 标准中的所定义的,这其实也是 ECMAScript 标准的作用之一,即定义 JavaScript 的标准语法。
词汇词法(Lexical Grammar)
ECMAScript 的词汇词法规定了 JavaScript 中的基础语法,比如哪些字符代表了空白(White Space),哪些字符代表了一行终止(Line Terminators),哪些字符的组合代表了注释(Comments)等。具体的规定说明,可以在 ECMAScript 标准 11 章中找到。
这里我们不仔细研读每个语法的定义,只需知道词法解析器(lexer)判读词组(token)的依据来源于此即可,为了让大家有一定的了解,这里,我们拿上面例子中的数字字面量(Numeric Literals)来进行说明:ECMAScript 标准中,对数字字面量的定义如下:该定义需要自上向下解读:
首先,规则定义了数字字面量(Numeric Literal)可以是十进制字面量(Decimal Literal)、二进制整数字面量(Binary Integer Literal)、八进制整数字面量(Octal Integer Literal)、十六进制整数字面量(Hex Integer Literal);
在我们的例子中,我们只关心十进制的字面量,所以,接下来,规则定义十进制字面量(Decimal Literal)可以是包含小数点与不包含小数点的组合,这里我们只需关注不包含小数点的定义,即十进制整数字面量(Decimal Integer Literal)+ 可选的指数部分(Exponent Part);
最后,规则定义十进制整数字母量由非零数字(Non Zero Digit)+ 十进制数字(Decimal Digit)或十进制数字组(Decimal Digits)组成,非零数字是由 1~9 的数字组成,十进制数字是由 0~9 组成。
将上面的定义重新整合后,就能得到我们需要的数字字面量的定义规则:
非零数字(1~9)+ 十进制数字组(0~9)
需要注意的是,这是简化版的数字字面量定义,完整版的需要加上以上规则中的所有分支条件。
表达式(Expressions)、语句(Statements)
ECMAScript 标准 12~13 章包含了表达式和语句的相关定义,之前由词法解析器(lexer)处理后生成的词组流(token)交由语法解释器(parser)处理的主要内容,即是处理词组流构成的表达式与语句。在这里,我们需要稍加明确一下表达式与语句之间的不同与关系:
首先,语句包含表达式,大部分语句是由关键字 + 表达式或语句组成,而表达式则是由字面量(Literal)、标识符(Identifier)、符号(Punctuators)等低一级的词组组成;
其次,表达式一般来讲会产生一个值,而语句不总有值。
理解第一点对于我们写语法解释器很重要,由于语句是由表达式组成的,而表达式是有词组组成的,词组是有词法解析器进行解析生成的,所以,在语法解释器中,将以表达式为切入点,由表达式解析再深入到语句解析中。
抽象语法树(AST)
了解一个 parser 的结构,以及 parser 解析语法所依赖的规则后,接下来,我们需要了解一下一个 parser 所生产出来的结果——抽象语法树。在文章的开头,我有简单的解释抽象语法树即是具体代码片段的抽象表达,那它具体是长什么样的呢?
function sum (a , b) {
return a+b;
}
以上的代码片段,AST 树的描述如下(使用 babylon7-7.0.0-beta.44,结果进行了简化):
{
“type”: “Program”,
“body”: [
{
“type”: “FunctionDeclaration”,
“id”: {
“type”: “Identifier”,
“name”: “sum”
},
“params”: [
{
“type”: “Identifier”,
“name”: “a”
},
{
“type”: “Identifier”,
“name”: “b”
}
],
“body”: {
“type”: “BlockStatement”,
“body”: [
{
“type”: “ReturnStatement”,
“argument”: {
“type”: “BinaryExpression”,
“left”: {
“type”: “Identifier”,
“name”: “a”
},
“operator”: “+”,
“right”: {
“type”: “Identifier”,
“name”: “b”
}
}
}
]
}
}
]
}
对该 AST 仔细观察一番,便会明白,AST 其实即是我们在已经 ECMAScript 标准对代码进行解析后,将标识符(identifier)、声明(declaration)、表达式(expression)、语句(statement)等按代码表述的逻辑整理成为树状结构。就拿上面的例子来说,当语法解析器识别了一个二元表达式(Binary Expression),便将这个二元表达式所携带的信息——左值,右值,操作符按照固定的计算机可读的数据格式保存下来,即是我们看到的 AST 树了。
当然,AST 也需要具备固定的格式,这样计算机才能依照该格式阅读 AST 并进行接下来的编译工作,当然,有一些 AST 也被用来转义(如 babel)。关于 AST 定义的规则,我们可以参考 babel 的定义,这也是后面我们实现 parser 时,所参考的标准。
接下来
理解完以上相关的知识,我们便具备编写一个 parser 的先决条件了,那在下一章,我们将实际操作一番,编写一个简易版本的 JavaScript 语言 parser。

正文完
 0