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

前面章節(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)照我的公眾號:


這里寫圖片描述
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評論 2 372

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