什么是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。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。