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