「譯」什麼是抽象語法樹

AST 是抽象語法樹的縮寫詞,表示編程語言的語句和表達式中生成的 token。有了 AST,解釋器或編譯器就能夠生成機器碼或者對一條指令求值。git

小貼士: 經過使用 Bit,你能夠將任意的 JS 代碼轉換爲一個可在項目和應用中共享、使用和同步的 API,從而更快地構建並重用更多代碼。試一下吧。github

假設咱們有下面這條簡單的表達式:算法

1 + 2
複製代碼

用 AST 來表示的話,它是這樣的:編程

+ BinaryExpression
 - type: +
 - left_value: 
  LiteralExpr:
   value: 1
 - right_vaue:
  LiteralExpr:
   value: 2
複製代碼

諸如 if 的語句則能夠像下面這樣表示:設計模式

if(2 > 6) {
    var d  = 90
    console.log(d)
}
IfStatement
 - condition
  + BinaryExpression
   - type: >
   - left_value: 2
   - right_value: 6
 - body
  [
    - Assign
        - left: 'd';
        - right: 
            LiteralExpr:
            - value: 90
    - MethodCall:
         - instanceName: console
         - methodName: log
         - args: [
         ]
  ]
複製代碼

這告訴解釋器如何解釋語句,同時告訴編譯器如何生成語句對應的代碼。數組

看看這條表達式: 1 + 2。咱們的大腦斷定這是一個將左值和右值相加的加法運算。如今,爲了讓計算機像咱們的大腦那樣工做,咱們必須以相似於大腦看待它的形式來表示它。bash

咱們用一個類來表示,其中的屬性告訴解釋器運算的所有內容、左值和右值。由於一個二元運算涉及兩個值,因此咱們給這個類命名爲 Binary編程語言

class Binary {  
    constructor(left, operator, right) {  
        this.left = left  
        this.operator = operator  
        this.right = right  
    }  
}
複製代碼

實例化期間,咱們將會把 1 傳給第一個屬性,把 ADD 傳給第二個屬性,把 2 傳給第三個屬性:函數

new Binary('1', 'ADD', '2')
複製代碼

當咱們把它傳遞給解釋器的時候,解釋器認爲這是一個二元運算,接着檢查操做符,認爲這是一個加法運算,緊接着繼續請求實例中的 left 值和 right 值,並將兩者相加:ui

const binExpr = new Binary('1', 'ADD', '2')

if(binExpr.operator == 'ADD') {  
    return binExpr.left + binExpr.right  
}  
// 返回 `3` 
複製代碼

看,AST 能夠像大腦那樣執行表達式和語句。

單數字、字符串、布爾值等都是表達式,它們能夠在 AST 中表示並求值。

23343
false
true
"nnamdi"
複製代碼

拿 1 舉例:

1
複製代碼

咱們在 AST 的 Literal(字面量) 類中來表示它。一個字面量就是一個單詞或者數字,Literal 類用一個屬性來保存它:

class Literal {  
    constructor(value) {  
        this.value = value  
    }  
}
複製代碼

咱們能夠像下面這樣表示 Literal 中的 1:

new Literal(1)
複製代碼

當解釋器對它求值時,它會請求 Literal 實例中 value 屬性的值:

const oneLit = new Literal(1)  
oneLit.value  
// `1`
複製代碼

在咱們的二元表達式中,咱們直接傳遞了值

new Binary('1', 'ADD', '2')
複製代碼

這其實並不合理。由於正如咱們在上面看到的,12 都是一條表達式,一條基本的表達式。做爲字面量,它們一樣須要被求值,而且用 Literal 類來表示。

const oneLit = new Literal('1')  
const twoLit = new Literal('2')
複製代碼

所以,二元表達式會將 oneLittwoLit 分別做爲左屬性和右屬性。

// ...  
new Binary(oneLit, 'ADD', twoLit)
複製代碼

在求值階段,左屬性和右屬性一樣須要進行求值,以得到各自的值:

const oneLit = new Literal('1')  
const twoLit = new Literal('2')  
const binExpr = new Binary(oneLit, 'ADD', twoLit)

if(binExpr.operator == 'ADD') {  
    return binExpr.left.value + binExpr.right.value  
}  
// 返回 `3` 
複製代碼

其它語句在 AST 中也大可能是用二元來表示的,例如 if 語句。

咱們知道,在 if 語句中,只有條件爲真的時候代碼塊纔會執行。

if(9 > 7) {  
    log('Yay!!')  
}
複製代碼

上面的 if 語句中,代碼塊執行的條件是 9 必須大於 7,以後咱們能夠在終端上看到輸出 Yay!!

爲了讓解釋器或者編譯器這樣執行,咱們將會在一個包含 conditionbody 屬性的類中來表示它。condition 保存着解析後必須爲真的條件,body 則是一個數組,它包含着 if 代碼塊中的全部語句。解釋器將會遍歷該數組並執行裏面的語句。

class IfStmt {  
    constructor(condition, body) {  
        this.condition = condition  
        this.body = body  
    }  
}
複製代碼

如今,讓咱們在 IfStmt 類中表示下面的語句

if(9 > 7) {  
    log('Yay!!')  
}
複製代碼

條件是一個二元運算,這將表示爲:

const cond = new Binary(new Literal(9), "GREATER", new Literal(7))
複製代碼

就像以前同樣,希望你還記得?這回是一個 GREATER 運算。

if 語句的代碼塊只有一條語句:一個函數調用。函數調用一樣能夠在一個類中表示,它包含的屬性有:用於指代所調用函數的 name 以及用於表示傳遞的參數的 args

class FuncCall {  
    constructor(name, args) {  
        this.name = name  
        this.args = args  
    }  
}
複製代碼

所以,log("Yay!!") 調用能夠表示爲:

const logFuncCall = new FuncCall('log', [])
複製代碼

如今,把這些組合在一塊兒,咱們的 if 語句就能夠表示爲:

const cond = new Binary(new Literal(9), "GREATER", new Literal(7));  
const logFuncCall = new FuncCall('log', []);

const ifStmt = new IfStmt(cond, [  
    logFuncCall  
])
複製代碼

解釋器能夠像下面這樣解釋 if 語句:

const ifStmt = new IfStmt(cond, [  
    logFuncCall  
])

function interpretIfStatement(ifStmt) {  
    if(evalExpr(ifStmt.conditon)) {  
        for(const stmt of ifStmt.body) {  
            evalStmt(stmt)  
        }  
    }  
}

interpretIfStatement(ifStmt)
複製代碼

輸出:

Yay!!
複製代碼

由於 9 > 7 :)

咱們經過檢查 condition 解析後是否爲真來解釋 if 語句。若是爲真,咱們遍歷 body 數組並執行裏面的語句。

執行 AST

使用訪問者模式對 AST 進行求值。訪問者模式是設計模式的一種,容許一組對象的算法在一個地方實現。

ASTs,Literal,Binary,IfStmnt 是一組相關的類,每個類都須要攜帶方法以使解釋器得到它們的值或者對它們求值。

訪問者模式讓咱們可以建立單個類,並在類中編寫 AST 的實現,將類提供給 AST。每一個 AST 都有一個公有的方法,解釋器會經過實現類實例對其進行調用,以後 AST 類將在傳入的實現類中調用相應的方法,從而計算其 AST。

class Literal {  
    constructor(value) {  
        this.value = value  
    }

    visit(visitor) {  
        return visitor.visitLiteral(this)  
    }  
}

class Binary {  
    constructor(left, operator, right) {  
        this.left = left  
        this.operator = operator  
        this.right = right  
    }

    visit(visitor) {  
        return visitor.visitBinary(this)  
    }  
}
複製代碼

看,AST Literal 和 Binary 都有訪問方法,可是在方法裏面,它們調用訪問者實例的方法來對自身求值。Literal 調用 visitLiteral,Binary 則調用 visitBinary

如今,將 Vistor 做爲實現類,它將實現 visitLiteral 和 visitBinary 方法:

class Visitor {

    visitBinary(binExpr) {  
        // ...  
        log('not yet implemented')  
    }

    visitLiteral(litExpr) {  
        // ...  
        log('not yet implemented')  
    }  
}
複製代碼

visitBinary 和 visitLiteral 在 Vistor 類中將會有本身的實現。所以,當一個解釋器想要解釋一個二元表達式時,它將調用二元表達式的訪問方法,並傳遞 Vistor 類的實例:

const binExpr = new Binary(...)  
const visitor = new Visitor()

binExpr.visit(visitor)
複製代碼

訪問方法將調用訪問者的 visitBinary,並將其傳遞給方法,以後打印 not yet implemented。這稱爲雙重分派。

  1. 調用 Binary 的訪問方法。
  2. 它 (Binary) 反過來調用 Visitor 實例的visitBinary

咱們把 visitLiteral 的完整代碼寫一下。因爲 Literal 實例的 value 屬性保存着值,因此這裏只需返回這個值就好:

class Visitor {

    visitBinary(binExpr) {  
        // ...  
        log('not yet implemented')  
    }

    visitLiteral(litExpr) {  
        return litExpr.value  
    }  
}
複製代碼

對於 visitBinary,咱們知道 Binary 類有操做符、左屬性和右屬性。操做符表示將對左右屬性進行的操做。咱們能夠編寫實現以下:

class Visitor {

    visitBinary(binExpr) {  
        switch(binExpr.operator) {  
            case 'ADD':  
            // ...  
        }  
    }

    visitLiteral(litExpr) {  
        return litExpr.value  
    }  
}
複製代碼

注意,左值和右值都是表達式,多是字面量表達式、二元表達式、調用表達式或者其它的表達式。咱們並不能確保二元運算的左右兩邊老是字面量。每個表達式必須有一個用於對錶達式求值的訪問方法,所以在上面的 visitBinary 方法中,咱們經過調用各自對應的 visit 方法對 Binary 的左屬性和右屬性進行求值:

class Visitor {

    visitBinary(binExpr) {  
        switch(binExpr.operator) {  
            case 'ADD':  
                return binExpr.left.visit(this) + binExpr.right.visit(this)  
        }  
    }

    visitLiteral(litExpr) {  
        return litExpr.value  
    }  
}
複製代碼

所以,不管左值和右值保存的是哪種表達式,最後均可以進行傳遞。

所以,若是咱們有下面這些語句:

const oneLit = new Literal('1')  
const twoLit = new Literal('2')  
const binExpr = new Binary(oneLit, 'ADD', twoLit)  
const visitor = new Visitor()

binExpr.visit(visitor)
複製代碼

在這種狀況下,二元運算保存的是字面量。

訪問者的 visitBinary 將會被調用,同時將 binExpr 傳入,在 Vistor 類中,visitBinary 將 oneLit 做爲左值,將 twoLit 做爲右值。因爲 oneLit 和 twoLit 都是 Literal 的實例,所以它們的訪問方法會被調用,同時將 Visitor 類傳入。對於 oneLit,其 Literal 類內部又會調用 Vistor 類的 visitLiteral 方法,並將 oneLit 傳入,而 Vistor 中的 visitLiteral 方法返回 Literal 類的 value 屬性,也就是 1。同理,對於 twoLit 來講,返回的是 2

由於執行了 switch 語句中的 case 'ADD',因此返回的值會相加,最後返回 3。

若是咱們將 binExpr.visit(visitor) 傳給 console.log,它將會打印 3

console.log(binExpr.visit(visitor))  
// 3
複製代碼

以下,咱們傳遞一個 3 分支的二元運算:

1 + 2 + 3
複製代碼

首先,咱們選擇 1 + 2,那麼其結果將做爲左值,即 + 3

上述能夠用 Binary 類表示爲:

new Binary (new Literal(1), 'ADD', new Binary(new Literal(2), 'ADD', new Literal(3)))
複製代碼

能夠看到,右值不是字面量,而是一個二元表達式。因此在執行加法運算以前,它必須先對這個二元表達式求值,並將其結果做爲最終求值時的右值。

const oneLit = new Literal(1)  
const threeLit =new Literal(3)  
const twoLit = new Literal(2)

const binExpr2 = new Binary(twoLit, 'ADD', threeLit)  
const binExpr1 = new Binary (oneLit, 'ADD', binExpr2)

const visitor = new Visitor()

log(binExpr1.visit(visitor))

6
複製代碼

添加 if 語句

if 語句帶到等式中。爲了對一個 if 語句求值,咱們將會給 IfStmt 類添加一個 visit 方法,以後它將調用 visitIfStmt 方法:

class IfStmt {  
    constructor(condition, body) {  
        this.condition = condition  
        this.body = body  
    }

    visit(visitor) {  
        return visitor.visitIfStmt(this)  
    }  
}
複製代碼

見識到訪問者模式的威力了嗎?咱們向一些類中新增了一個類,對應地只須要添加相同的訪問方法便可,而這將調用它位於 Vistor 類中的對應方法。這種方式將不會破壞或者影響到其它的相關類,訪問者模式讓咱們遵循了開閉原則。

所以,咱們在 Vistor 類中實現 visitIfStmt

class Visitor {  
    // ...

    visitIfStmt(ifStmt) {  
        if(ifStmt.condition.visit(this)) {  
            for(const stmt of ifStmt.body) {  
                stmt.visit(this)  
            }  
        }  
    }  
}
複製代碼

由於條件是一個表達式,因此咱們調用它的訪問方法對其進行求值。咱們使用 JS 中的 if 語句檢查返回值,若是爲真,則遍歷語句的代碼塊 ifStmt.body,經過調用 visit 方法並傳入 Vistor,對數組中每一條語句進行求值。

所以咱們能夠翻譯出這條語句:

if(67 > 90)
複製代碼

添加函數調用和函數聲明

接着來添加一個函數調用。咱們已經有一個對應的類了:

class FuncCall {  
    constructor(name, args) {  
        this.name = name  
        this.args = args  
    }  
}
複製代碼

添加一個訪問方法:

class FuncCall {  
    constructor(name, args) {  
        this.name = name  
        this.args = args  
    }

    visit(visitor) {  
        return visitor.visitFuncCall(this)  
    }  
}
複製代碼

Visitor 類添加 visitFuncCall 方法:

class Visitor {  
    // ...

    visitFuncCall(funcCall) {  
        const funcName = funcCall.name  
        const args = []  
        for(const expr of funcCall.args)  
            args.push(expr.visit(this))  
        // ...  
    }  
}
複製代碼

這裏有一個問題。除了內置函數以外,還有自定義函數,咱們須要爲後者建立一個「容器」,並在裏面經過函數名保存和引用該函數。

const FuncStore = (  
    class FuncStore {

        constructor() {  
            this.map = new Map()  
        }

        setFunc(name, body) {  
            this.map.set(name, body)  
        }

        getFunc(name) {  
            return this.map.get(name)  
        }  
    }  
    return new FuncStore()  
)()
複製代碼

FuncStore 保存着函數,並從一個 Map 實例中取回這些函數。

class Visitor {  
    // ...

    visitFuncCall(funcCall) {  
        const funcName = funcCall.name  
        const args = []  
        for(const expr of funcCall.args)  
            args.push(expr.visit(this))  
        if(funcName == "log")  
            console.log(...args)  
        if(FuncStore.getFunc(funcName))  
            FuncStore.getFunc(funcName).forEach(stmt => stmt.visit(this))  
    }  
}
複製代碼

看下咱們作了什麼。若是函數名 funcName(記住,FuncCall 類將函數名保存在 name 屬性中)爲 log,則運行 JS console.log(...),並傳參給它。若是咱們在函數保存中找到了函數,那麼就對該函數體進行遍歷,依次訪問並執行。

如今看看怎麼把咱們的函數聲明放進函數保存中。

函數聲明以 fucntion 開頭。通常的函數結構是這樣的:

function function_name(params) {  
    // function body  
}
複製代碼

所以,咱們能夠在一個類中用屬性表示一個函數聲明:name 保存函數函數名,body 則是一個數組,保存函數體中的語句:

class FunctionDeclaration {  
    constructor(name, body) {  
        this.name = name  
        this.body = body  
    }  
}
複製代碼

咱們添加一個訪問方法,該方法在 Vistor 中被稱爲 visitFunctionDeclaration:

class FunctionDeclaration {  
    constructor(name, body) {  
        this.name = name  
        this.body = body  
    }

    visit(visitor) {  
        return visitor.visitFunctionDeclaration(this)  
    }  
}
複製代碼

在 Visitor 中:

class Visitor {  
    // ...

    visitFunctionDeclaration(funcDecl) {  
        FuncStore.setFunc(funcDecl.name, funcDecl.body)  
    }  
}
複製代碼

將函數名做爲鍵便可保存函數。

如今,假設咱們有下面這個函數:

function addNumbers(a, b) {  
    log(a + b)  
}

function logNumbers() {  
    log(5)  
    log(6)  
}
複製代碼

它能夠表示爲:

const funcDecl = new FunctionDeclaration('logNumbers', [  
    new FuncCall('log', [new Literal(5)]),  
    new FuncCall('log', [new Literal(6)])  
])

visitor.visitFunctionDeclaration(funcDecl)
複製代碼

如今,咱們來調用函數 logNumbers

const funcCall = new FuncCall('logNumbers', [])  
visitor.visitFuncCall(funcCall)
複製代碼

控制檯將會打印:

5
6
複製代碼

結論

理解 AST 的過程是使人望而生畏而且很是消耗腦力的。即便是編寫最簡單的解析器也須要大量的代碼。

注意,咱們並無介紹掃描儀和解析器,而是先行解釋了 ASTs 以展現它們的工做過程。若是你可以深刻理解 AST 以及它所須要的內容,那麼在你開始編寫本身的編程語言時,天然就事半功倍了。

熟能生巧,你能夠繼續添加其它的編程語言特性,例如:

  • 類和對象
  • 方法調用
  • 封裝和繼承
  • for-of 語句
  • while 語句
  • for-in 語句
  • 其它任何你能想到的有趣特性

若是你對此有任何疑問,或者是任何我須要添加、修改、刪減的內容,歡迎評論和致郵。

感謝 !!!

相關文章
相關標籤/搜索