前面章节中,我们完成了词法解析器的开发。词法解析的目的是把程序代码中的各个字符串进行识别分类,把不同字符串归纳到相应的分类中,例如数字构成的字符串统一归类为INTEGER, 字符构成的字符串,如果不是关键字的话,那么他们统一被归纳为IDENTIFIER。
例如下面这条语句:
代码语言:javascript复制let foo = 1234;
语句经过词法解析器解析后,就会转变为:
代码语言:javascript复制LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI
完成上面工作后,词法解析器的任务就完成了,接下来就轮到词法解析器出场。词法解析器的作用是,判断上面的分类组合是否合法。显然上面分类的组合次序是合法的,但是对于下面语句:
代码语言:javascript复制let foo 1234;
词法解析得到的分类组合为:
代码语言:javascript复制LET IDENTIFIER PLUS_SIGN INTEGER SEMI
显然上面的组合是错误的,语法解析器就是要检测到上面这些错误组合。如果组合是正确的,那么语法解析器还会根据组合所形成的逻辑关系构造出一种数据结构叫抽象语法树,其本质就是一种多叉树,有了这种数据结构,编译器就可以为 代码生成二进制指令,或者直接对程序进行解释执行。
事实上,每一句代码的背后都遵循着严谨的逻辑结构。例如当你看到关键字 let 时,你一定知道,在后面跟着的必须是一个字符串变量,如果let 后面跟着一个数字,那就是一种语法错误。这种规则的表现方式就叫语法表达式,例如let 语句的语法表达式如下:
代码语言:javascript复制LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI
EXPRESSION 用来表示可以放在等号后面进行赋值的代码字符串,它可以是一个数字,一个变量字符串,也可以是一串复杂的算术表达式。上面这种语法表达式也叫Backus-Naur 范式,其中Backus是IBM的研究员,是他发明了第一个编译器,用来编译Fortan 语言。大家注意看,语法表达式其实隐含着一种递归结构,上面表达式中右边的EXPRESSION 其实还可以继续分解,相关的内容我们会在后面给出。
上面的语法表达式其实也可以对应成一颗多叉树,树的父节点就是左边的LetStatment,右边的五个分类对应于叶子节点,其中EXPRESSION有可以继续分解,于是它自己就是多叉树中,一颗子树的父节点。在后续的课程中,我们会用代码亲自绘制出对应的多叉树。
链接:http://tomcopeland.blogs.com/EcmaScript.html 描述的就是javascript语言的语法表达式,有兴趣的同学可以点进去看看。
语法解析的本质就是,先让词法解析器把代码字符串解析成各种分类的组合,然后根据早已给定的语法表达式所定义的语法规则,看看分类的组合方式是否符合语法表达式的规定。我们本节将实现一个简单的语法解析器,它的作用是能解析let 语句,例如:
代码语言:javascript复制let foo = 1234;
let x = y;
语法解析器在实现语法解析时,一般有两种策略,一种叫自顶向下,一种是自底向上。我们将采取自顶向下的做法,语法解析是编译原理中最为抽象的模块,一定得通过代码调试来加深理解,以下就是我们实现let 语句解析的语法解析器代码,首先在本地目录src下面新建一个文件名为MonkeyCompilerParser.js的文件,并添加如下代码:
代码语言:javascript复制class Node {
constructor (props) {
this.tokenLiteral = ""
}
getLiteral() {
return this.tokenLiteral
}
}
class Statement extends Node{
statementNode () {
return this
}
}
class Expression extends Node{
constructor(props) {
super(props)
this.tokenLiteral = props.token.getLiteral()
}
expressionNode () {
return this
}
}
class Identifier extends Expression {
constructor(props) {
super(props)
this.tokenLiteral = props.token.getLiteral()
this.token = props.token
this.value = ""
}
}
由于语法解析的结果是要构造一颗多叉树,因此类Node用来表示多叉树的叶子节点,Statement 和 Expression依次继承Node,注意看Expression的代码,我们要解析的语句形式如下:
代码语言:javascript复制let foo = 1234;
它对应的语法表达式为:
代码语言:javascript复制LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI
我们前面提到EXPRESSION 可以表示一个变量,一个整数,或者是一个复杂的算式表达式,对于上面我们要解析的语句,等号后面是1234,它对应的分类就是INTEGER, 于是我们可以猜测,上面Expression类的构造函数constructor中,props.token对应的就是INTEGER, 于是getLiteral()得到的就是分类INTEGER对应的数字字符串,也就是1234.
Identifier类对应的就是语法表达式LET 后面的IDENTIFIER分类,对应我们给出的例子,let 后面跟着变量字符串foo, 于是我们可以猜测,Identifier类的构造函数中,props.token 对应的就是 IDENTIFIER , token.getLiteral() 得到的就是变量字符串 “foo”
我们继续添加相应代码:
代码语言:javascript复制class LetStatement extends Statement {
constructor(props) {
super(props)
this.token = props.token
this.name = props.identifier
this.value = props.expression
var s = "This is a Let statement, left is an identifer:"
s = props.identifer.getLiteral()
s = " right size is value of "
s = this.value.getLiteral()
this.tokenLiteral = s
}
}
LetStatement类用来表示与let 相关的语句,从语法表达式可以看成,let 语句由两个关键部分组成,一个是let关键字后面的变量,一个是等号后面的数值或者是变量,或者是算术表达式。因此在上面的LetStatement类中,props.token 对应的就是关键字 LET, props.identifier对应的就是类Identifier的实例,其实也就是let关键字后面的变量,props.expression 对应的是等号后面的成分,对应到我们的具体实例中,它就是一个数字,也就是INTEGER.
我们继续添加代码:
代码语言:javascript复制class Program {
constructor () {
this.statements = []
}
getLiteral() {
if (this.statements.length > 0) {
return this.statements[0].tokenLiteral()
} else {
return ""
}
}
}
Program 类是对整个程序代码的抽象,它由一系列Statement组成,Statement基本可以理解为一句以分号结束的代码。于是整个程序就是由很多条以分号结束的语句代码的集合。当然有一些不已分号结束的语句也是Statement,例如:
代码语言:javascript复制if (x == 10) {...}
else {...}
此类语句也是属于Statement。 接着我们就进入解析器的实现部分:
代码语言:javascript复制class MonkeyCompilerParser {
constructor(lexer) {
this.lexer = lexer
this.lexer.lexing()
this.tokenPos = 0
this.curToken = null
this.peekToken = null
this.nextToken()
this.nextToken()
this.program = new Program()
}
nextToken() {
/*
一次必须读入两个token,这样我们才了解当前解析代码的意图
例如假设当前解析的代码是 5; 那么peekToken就对应的就是
分号,这样解析器就知道当前解析的代码表示一个整数
*/
this.curToken = this.peekToken
this.peekToken = this.lexer.tokens[this.tokenPos]
this.tokenPos
}
parseProgram() {
while (this.curToken.getType() !== this.lexer.EOF) {
var stmt = this.parseStatement()
if (stmt !== null) {
this.program.statements.push(stmt)
}
this.nextToken()
}
return this.program
}
parseStatement() {
switch (this.curToken.getType()) {
case this.lexer.LET:
return this.parseLetStatement()
default:
return null
}
}
parseLetStatement() {
var props = {}
props.token = this.curToken
//expectPeek 会调用nextToken将curToken转换为
//下一个token
if (!this.expectPeek(this.lexer.IDENTIFIER)) {
return null
}
var identProps = {}
identProps.token = this.curToken
identProps.value = this.curToken.getLiteral()
props.identifer = new Identifier(identProps)
if (!this.expectPeek(this.lexer.ASSIGN_SIGN)) {
return null
}
if (!this.expectPeek(this.lexer.INTEGER)) {
return null
}
var exprProps = {}
exprProps.token = this.curToken
props.expression = new Expression(exprProps)
if (!this.expectPeek(this.lexer.SEMICOLON)) {
return null
}
var letStatement = new LetStatement(props)
return letStatement
}
curTokenIs (tokenType) {
return this.curToken.getType() === tokenType
}
peekTokenIs(tokenType) {
return this.peekToken.getType() === tokenType
}
expectPeek(tokenType) {
if (this.peekTokenIs(tokenType)) {
this.nextToken()
return true
} else {
return false
}
}
}
解析器在构造时,需要传入词法解析器,因为解析器解析的内容是经过词法解析器处理后的结果,也就是一系列token的组合。它在构造函数中,先调用解析器的lexing()接口,先对代码进行词法解析,词法解析会把源代码解析成一系列token的组合,curToken用于指向词法解析器对代码进行解析后得到的token数组中的某一个,而peekToken指向curToken指向token的下一个token。
接着连续两次调用nextToken,目的是让curToken指向词法解析器解析得到的token数组中的第一个token,peekToken指向第二个token, 当parseProgram被调用时,程序就启动了词法解析的过程。在该函数中,每次取出一个token,如果当前token代表的不是程序结束标志的话,它就调用parseStatement来解析一条以语句。
在parseStatement中,它会根据当前读入的token类型来进行不同的操作,如果读到的当前token是一个关键字let, 那意味着,解析器当前读到了一条以let开始的变量定义语句,于是解析器接下来就要检测后面一系列token的组合关系是否符合let 语句语法表达式指定的规范,负责这个检测任务的就是函数parseLetStatement()。
parseLetStatement函数的实现逻辑严格遵守语法表达式的规定。
代码语言:javascript复制LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI
我们看上面的表达式,它表明,一个let 语句必须以let 关键字开头,然后必须跟着一个变量字符串,接着必须跟着一个等号,然后等号右边是一个算术表达式,最后必须以分号结尾,这个组合关系只要有某部分不对应,那么就出现了语法错误。
在调用parseLetStatement之前的函数parseStatement里面的switch 语句里已经判断第一步,也就是语句确实是以关键字let开始之后,才会进入parseLetStatement,
代码语言:javascript复制if (!this.expectPeek(this.lexer.IDENTIFIER)) {
return null
}
上面代码用于判断,跟着关键字let 后面的是不是变量字符串,也就是对应的token是否是IDENTIFIER, 如果不是,解析出错直接返回。如果是就用当前的token构建一个Identifier类,并把它作为初始化LetStatement类的一部分。接下来就得判断跟着的是否是等号了:
代码语言:javascript复制if (!this.expectPeek(this.lexer.ASSIGN_SIGN)) {
return null
}
上面代码片段就是用来判断跟在变量字符串后面的是否是等号,如果不是,那么语法错误,直接返回。在等号后面必须跟着一个算术表达式,算术表达式又可以分解为一个数字常量字符串,一个变量字符串,或者是由变量字符串和数字常量字符串结合各种运算符所组成的算术式子,由于为了简单起见,我们现在只支持等号后面跟着数字常量表达式,也就是我们现在的解析器只能支持解析类似如下的语句:
代码语言:javascript复制let foo = 1234;
let x = 2;
对于型如以下合法的let语句解析,我们将在后续章节再给出:
代码语言:javascript复制let foo = bar;
let bar = 2*3 foo / 2;
如果等号后面跟着的字符串确实是一个数字常量字符串,那么我们就构造一个Expression类,这个类会成为LetStatement类的组成部分。根据语法表达式规定,let 语句最后要以分号结尾,因此代码片段:
代码语言:javascript复制if (!this.expectPeek(this.lexer.SEMICOLON)) {
return null
}
其作用就是用于判断末尾是否是分号,如果不是的话,那就出现了语法错误。
上面代码完成后,我们需要在MonkeyCompilerIDE 组件中引入语法解析器,并将用户在编辑框中输入的代码提交给解析器进行解析,因此相关改动如下:
代码语言:javascript复制import MonkeyCompilerParser from './MonkeyCompilerParser'
class MonkeyCompilerIDE extends Component {
....
// change here
onLexingClick () {
this.lexer = new MonkeyLexer(this.inputInstance.getContent())
this.parser = new MonkeyCompilerParser(this.lexer)
this.parser.parseProgram()
this.program = this.parser.program
for (var i = 0; i < this.program.statements.length; i ) {
console.log(this.program.statements[i].getLiteral())
}
}
....
render () {
// change here
return (
<bootstrap.Panel header="Monkey Compiler" bsStyle="success">
<MonkeyCompilerEditer
ref={(ref) => {this.inputInstance = ref}}
keyWords={this.lexer.getKeyWords()}/>
<bootstrap.Button onClick={this.onLexingClick.bind(this)}
style={{marginTop: '16px'}}
bsStyle="danger">
Parsing
</bootstrap.Button>
</bootstrap.Panel>
);
}
}
一旦用户点击下面红色按钮时,解析器就启动了语法解析过程,解析完后,解析器会返回一个Program类,该类里面包含了解析器把语句解析后所得到的结果,Program类里面的statments数组存储的就是每条语句被语法解析器解析后的结果,我们通过遍历statements数组里面每个元素,由于他们都继承自类Node,因此他们都实现了getLiteral接口,通过这个接口,我们可以把解析器对每条语句的解析结果输出到控制台上。
上面代码完成后,加载页面,在编辑框中输入如下内容:
然后点击下方的红色”Parsing”按钮,开始解析,接着打开控制台,我们就能看到相应的输出结果:
由于语法解析是编译原理中较为抽象难理解的部分,大家一定要根据视频讲解,对代码进行亲自调试,唯有如此,你才能对语法解析有比较深入和直观的了解。