前面章節(jié)中,我們完成了詞法解析器的開(kāi)發(fā)。詞法解析的目的是把程序代碼中的各個(gè)字符串進(jìn)行識(shí)別分類,把不同字符串歸納到相應(yīng)的分類中,例如數(shù)字構(gòu)成的字符串統(tǒng)一歸類為INTEGER, 字符構(gòu)成的字符串,如果不是關(guān)鍵字的話,那么他們統(tǒng)一被歸納為IDENTIFIER。
例如下面這條語(yǔ)句:
let foo = 1234;
語(yǔ)句經(jīng)過(guò)詞法解析器解析后,就會(huì)轉(zhuǎn)變?yōu)椋?/p>
LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI
完成上面工作后,詞法解析器的任務(wù)就完成了,接下來(lái)就輪到詞法解析器出場(chǎng)。詞法解析器的作用是,判斷上面的分類組合是否合法。顯然上面分類的組合次序是合法的,但是對(duì)于下面語(yǔ)句:
let foo + 1234;
詞法解析得到的分類組合為:
LET IDENTIFIER PLUS_SIGN INTEGER SEMI
顯然上面的組合是錯(cuò)誤的,語(yǔ)法解析器就是要檢測(cè)到上面這些錯(cuò)誤組合。如果組合是正確的,那么語(yǔ)法解析器還會(huì)根據(jù)組合所形成的邏輯關(guān)系構(gòu)造出一種數(shù)據(jù)結(jié)構(gòu)叫抽象語(yǔ)法樹(shù),其本質(zhì)就是一種多叉樹(shù),有了這種數(shù)據(jù)結(jié)構(gòu),編譯器就可以為
代碼生成二進(jìn)制指令,或者直接對(duì)程序進(jìn)行解釋執(zhí)行。
事實(shí)上,每一句代碼的背后都遵循著嚴(yán)謹(jǐn)?shù)倪壿嫿Y(jié)構(gòu)。例如當(dāng)你看到關(guān)鍵字 let 時(shí),你一定知道,在后面跟著的必須是一個(gè)字符串變量,如果let 后面跟著一個(gè)數(shù)字,那就是一種語(yǔ)法錯(cuò)誤。這種規(guī)則的表現(xiàn)方式就叫語(yǔ)法表達(dá)式,例如let 語(yǔ)句的語(yǔ)法表達(dá)式如下:
LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI
EXPRESSION 用來(lái)表示可以放在等號(hào)后面進(jìn)行賦值的代碼字符串,它可以是一個(gè)數(shù)字,一個(gè)變量字符串,也可以是一串復(fù)雜的算術(shù)表達(dá)式。上面這種語(yǔ)法表達(dá)式也叫Backus-Naur 范式,其中Backus是IBM的研究員,是他發(fā)明了第一個(gè)編譯器,用來(lái)編譯Fortan 語(yǔ)言。大家注意看,語(yǔ)法表達(dá)式其實(shí)隱含著一種遞歸結(jié)構(gòu),上面表達(dá)式中右邊的EXPRESSION 其實(shí)還可以繼續(xù)分解,相關(guān)的內(nèi)容我們會(huì)在后面給出。
上面的語(yǔ)法表達(dá)式其實(shí)也可以對(duì)應(yīng)成一顆多叉樹(shù),樹(shù)的父節(jié)點(diǎn)就是左邊的LetStatment,右邊的五個(gè)分類對(duì)應(yīng)于葉子節(jié)點(diǎn),其中EXPRESSION有可以繼續(xù)分解,于是它自己就是多叉樹(shù)中,一顆子樹(shù)的父節(jié)點(diǎn)。在后續(xù)的課程中,我們會(huì)用代碼親自繪制出對(duì)應(yīng)的多叉樹(shù)。
鏈接:http://tomcopeland.blogs.com/EcmaScript.html 描述的就是javascript語(yǔ)言的語(yǔ)法表達(dá)式,有興趣的同學(xué)可以點(diǎn)進(jìn)去看看。
語(yǔ)法解析的本質(zhì)就是,先讓詞法解析器把代碼字符串解析成各種分類的組合,然后根據(jù)早已給定的語(yǔ)法表達(dá)式所定義的語(yǔ)法規(guī)則,看看分類的組合方式是否符合語(yǔ)法表達(dá)式的規(guī)定。我們本節(jié)將實(shí)現(xiàn)一個(gè)簡(jiǎn)單的語(yǔ)法解析器,它的作用是能解析let 語(yǔ)句,例如:
let foo = 1234;
let x = y;
語(yǔ)法解析器在實(shí)現(xiàn)語(yǔ)法解析時(shí),一般有兩種策略,一種叫自頂向下,一種是自底向上。我們將采取自頂向下的做法,語(yǔ)法解析是編譯原理中最為抽象的模塊,一定得通過(guò)代碼調(diào)試來(lái)加深理解,以下就是我們實(shí)現(xiàn)let 語(yǔ)句解析的語(yǔ)法解析器代碼,首先在本地目錄src下面新建一個(gè)文件名為MonkeyCompilerParser.js的文件,并添加如下代碼:
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 = ""
}
}
由于語(yǔ)法解析的結(jié)果是要構(gòu)造一顆多叉樹(shù),因此類Node用來(lái)表示多叉樹(shù)的葉子節(jié)點(diǎn),Statement 和 Expression依次繼承Node,注意看Expression的代碼,我們要解析的語(yǔ)句形式如下:
let foo = 1234;
它對(duì)應(yīng)的語(yǔ)法表達(dá)式為:
LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI
我們前面提到EXPRESSION 可以表示一個(gè)變量,一個(gè)整數(shù),或者是一個(gè)復(fù)雜的算式表達(dá)式,對(duì)于上面我們要解析的語(yǔ)句,等號(hào)后面是1234,它對(duì)應(yīng)的分類就是INTEGER, 于是我們可以猜測(cè),上面Expression類的構(gòu)造函數(shù)constructor中,props.token對(duì)應(yīng)的就是INTEGER, 于是getLiteral()得到的就是分類INTEGER對(duì)應(yīng)的數(shù)字字符串,也就是1234.
Identifier類對(duì)應(yīng)的就是語(yǔ)法表達(dá)式LET 后面的IDENTIFIER分類,對(duì)應(yīng)我們給出的例子,let 后面跟著變量字符串foo, 于是我們可以猜測(cè),Identifier類的構(gòu)造函數(shù)中,props.token 對(duì)應(yīng)的就是 IDENTIFIER , token.getLiteral() 得到的就是變量字符串 "foo"
我們繼續(xù)添加相應(yīng)代碼:
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類用來(lái)表示與let 相關(guān)的語(yǔ)句,從語(yǔ)法表達(dá)式可以看成,let 語(yǔ)句由兩個(gè)關(guān)鍵部分組成,一個(gè)是let關(guān)鍵字后面的變量,一個(gè)是等號(hào)后面的數(shù)值或者是變量,或者是算術(shù)表達(dá)式。因此在上面的LetStatement類中,props.token 對(duì)應(yīng)的就是關(guān)鍵字 LET, props.identifier對(duì)應(yīng)的就是類Identifier的實(shí)例,其實(shí)也就是let關(guān)鍵字后面的變量,props.expression 對(duì)應(yīng)的是等號(hào)后面的成分,對(duì)應(yīng)到我們的具體實(shí)例中,它就是一個(gè)數(shù)字,也就是INTEGER.
我們繼續(xù)添加代碼:
class Program {
constructor () {
this.statements = []
}
getLiteral() {
if (this.statements.length > 0) {
return this.statements[0].tokenLiteral()
} else {
return ""
}
}
}
Program 類是對(duì)整個(gè)程序代碼的抽象,它由一系列Statement組成,Statement基本可以理解為一句以分號(hào)結(jié)束的代碼。于是整個(gè)程序就是由很多條以分號(hào)結(jié)束的語(yǔ)句代碼的集合。當(dāng)然有一些不已分號(hào)結(jié)束的語(yǔ)句也是Statement,例如:
if (x == 10) {...}
else {...}
此類語(yǔ)句也是屬于Statement。 接著我們就進(jìn)入解析器的實(shí)現(xiàn)部分:
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() {
/*
一次必須讀入兩個(gè)token,這樣我們才了解當(dāng)前解析代碼的意圖
例如假設(shè)當(dāng)前解析的代碼是 5; 那么peekToken就對(duì)應(yīng)的就是
分號(hào),這樣解析器就知道當(dāng)前解析的代碼表示一個(gè)整數(shù)
*/
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 會(huì)調(diào)用nextToken將curToken轉(zhuǎn)換為
//下一個(gè)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
}
}
}
解析器在構(gòu)造時(shí),需要傳入詞法解析器,因?yàn)榻馕銎鹘馕龅膬?nèi)容是經(jīng)過(guò)詞法解析器處理后的結(jié)果,也就是一系列token的組合。它在構(gòu)造函數(shù)中,先調(diào)用解析器的lexing()接口,先對(duì)代碼進(jìn)行詞法解析,詞法解析會(huì)把源代碼解析成一系列token的組合,curToken用于指向詞法解析器對(duì)代碼進(jìn)行解析后得到的token數(shù)組中的某一個(gè),而peekToken指向curToken指向token的下一個(gè)token。
接著連續(xù)兩次調(diào)用nextToken,目的是讓curToken指向詞法解析器解析得到的token數(shù)組中的第一個(gè)token,peekToken指向第二個(gè)token, 當(dāng)parseProgram被調(diào)用時(shí),程序就啟動(dòng)了詞法解析的過(guò)程。在該函數(shù)中,每次取出一個(gè)token,如果當(dāng)前token代表的不是程序結(jié)束標(biāo)志的話,它就調(diào)用parseStatement來(lái)解析一條以語(yǔ)句。
在parseStatement中,它會(huì)根據(jù)當(dāng)前讀入的token類型來(lái)進(jìn)行不同的操作,如果讀到的當(dāng)前token是一個(gè)關(guān)鍵字let, 那意味著,解析器當(dāng)前讀到了一條以let開(kāi)始的變量定義語(yǔ)句,于是解析器接下來(lái)就要檢測(cè)后面一系列token的組合關(guān)系是否符合let 語(yǔ)句語(yǔ)法表達(dá)式指定的規(guī)范,負(fù)責(zé)這個(gè)檢測(cè)任務(wù)的就是函數(shù)parseLetStatement()。
parseLetStatement函數(shù)的實(shí)現(xiàn)邏輯嚴(yán)格遵守語(yǔ)法表達(dá)式的規(guī)定。
LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI
我們看上面的表達(dá)式,它表明,一個(gè)let 語(yǔ)句必須以let 關(guān)鍵字開(kāi)頭,然后必須跟著一個(gè)變量字符串,接著必須跟著一個(gè)等號(hào),然后等號(hào)右邊是一個(gè)算術(shù)表達(dá)式,最后必須以分號(hào)結(jié)尾,這個(gè)組合關(guān)系只要有某部分不對(duì)應(yīng),那么就出現(xiàn)了語(yǔ)法錯(cuò)誤。
在調(diào)用parseLetStatement之前的函數(shù)parseStatement里面的switch 語(yǔ)句里已經(jīng)判斷第一步,也就是語(yǔ)句確實(shí)是以關(guān)鍵字let開(kāi)始之后,才會(huì)進(jìn)入parseLetStatement,
if (!this.expectPeek(this.lexer.IDENTIFIER)) {
return null
}
上面代碼用于判斷,跟著關(guān)鍵字let 后面的是不是變量字符串,也就是對(duì)應(yīng)的token是否是IDENTIFIER, 如果不是,解析出錯(cuò)直接返回。如果是就用當(dāng)前的token構(gòu)建一個(gè)Identifier類,并把它作為初始化LetStatement類的一部分。接下來(lái)就得判斷跟著的是否是等號(hào)了:
if (!this.expectPeek(this.lexer.ASSIGN_SIGN)) {
return null
}
上面代碼片段就是用來(lái)判斷跟在變量字符串后面的是否是等號(hào),如果不是,那么語(yǔ)法錯(cuò)誤,直接返回。在等號(hào)后面必須跟著一個(gè)算術(shù)表達(dá)式,算術(shù)表達(dá)式又可以分解為一個(gè)數(shù)字常量字符串,一個(gè)變量字符串,或者是由變量字符串和數(shù)字常量字符串結(jié)合各種運(yùn)算符所組成的算術(shù)式子,由于為了簡(jiǎn)單起見(jiàn),我們現(xiàn)在只支持等號(hào)后面跟著數(shù)字常量表達(dá)式,也就是我們現(xiàn)在的解析器只能支持解析類似如下的語(yǔ)句:
let foo = 1234;
let x = 2;
對(duì)于型如以下合法的let語(yǔ)句解析,我們將在后續(xù)章節(jié)再給出:
let foo = bar;
let bar = 2*3 + foo / 2;
如果等號(hào)后面跟著的字符串確實(shí)是一個(gè)數(shù)字常量字符串,那么我們就構(gòu)造一個(gè)Expression類,這個(gè)類會(huì)成為L(zhǎng)etStatement類的組成部分。根據(jù)語(yǔ)法表達(dá)式規(guī)定,let 語(yǔ)句最后要以分號(hào)結(jié)尾,因此代碼片段:
if (!this.expectPeek(this.lexer.SEMICOLON)) {
return null
}
其作用就是用于判斷末尾是否是分號(hào),如果不是的話,那就出現(xiàn)了語(yǔ)法錯(cuò)誤。
上面代碼完成后,我們需要在MonkeyCompilerIDE 組件中引入語(yǔ)法解析器,并將用戶在編輯框中輸入的代碼提交給解析器進(jìn)行解析,因此相關(guān)改動(dòng)如下:
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>
);
}
}
一旦用戶點(diǎn)擊下面紅色按鈕時(shí),解析器就啟動(dòng)了語(yǔ)法解析過(guò)程,解析完后,解析器會(huì)返回一個(gè)Program類,該類里面包含了解析器把語(yǔ)句解析后所得到的結(jié)果,Program類里面的statments數(shù)組存儲(chǔ)的就是每條語(yǔ)句被語(yǔ)法解析器解析后的結(jié)果,我們通過(guò)遍歷statements數(shù)組里面每個(gè)元素,由于他們都繼承自類Node,因此他們都實(shí)現(xiàn)了getLiteral接口,通過(guò)這個(gè)接口,我們可以把解析器對(duì)每條語(yǔ)句的解析結(jié)果輸出到控制臺(tái)上。
上面代碼完成后,加載頁(yè)面,在編輯框中輸入如下內(nèi)容:
然后點(diǎn)擊下方的紅色"Parsing"按鈕,開(kāi)始解析,接著打開(kāi)控制臺(tái),我們就能看到相應(yīng)的輸出結(jié)果:
由于語(yǔ)法解析是編譯原理中較為抽象難理解的部分,大家一定要根據(jù)視頻講解,對(duì)代碼進(jìn)行親自調(diào)試,唯有如此,你才能對(duì)語(yǔ)法解析有比較深入和直觀的了解。
更詳細(xì)的講解和代碼調(diào)試演示過(guò)程,請(qǐng)點(diǎn)擊鏈接
更多技術(shù)信息,包括操作系統(tǒng),編譯器,面試算法,機(jī)器學(xué)習(xí),人工智能,請(qǐng)關(guān)照我的公眾號(hào):