Reactjs開(kāi)發(fā)自制編程語(yǔ)言Monkey的編譯器:語(yǔ)法解析

前面章節(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)容:

這里寫(xiě)圖片描述

然后點(diǎn)擊下方的紅色"Parsing"按鈕,開(kāi)始解析,接著打開(kāi)控制臺(tái),我們就能看到相應(yīng)的輸出結(jié)果:

這里寫(xiě)圖片描述

由于語(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):


這里寫(xiě)圖片描述
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容