教你使用swift寫編譯器玩具(2)

前言

本章對應官方教程第2章,介紹實現解析器(Parser)和抽象語法樹(AST)。html

教程以下:git

教你使用swift寫編譯器玩具(0)github

教你使用swift寫編譯器玩具(1)express

教你使用swift寫編譯器玩具(2)swift

教你使用swift寫編譯器玩具(3)app

教你使用swift寫編譯器玩具(4)ide

教你使用swift寫編譯器玩具(5)函數

教你使用swift寫編譯器玩具(6)post

教你使用swift寫編譯器玩具(7)ui

教你使用swift寫編譯器玩具(8)

倉庫在這

開始

AST

AST用來解釋代碼的行爲,咱們但願語言中的每一個構造都有一個AST,因此咱們首先須要一個AST的基類,在swift中咱們可使用protocol

protocol ExprAST {}
複製代碼

注意,在Kaleidoscope中咱們只支持Double類型,因此首先咱們須要有一個保存數值的AST。

class NumberExprAST: ExprAST {
    
    let value: Double
    
    init(_ value: Double) {
        self.value = value
    }
    
}
複製代碼

用於保存變量名的AST,好比說保存"abc"。

class VariableExprAST: ExprAST {
    
    let name: String
    
    init(_ name: String) {
        self.name = name
    }
    
}
複製代碼

用於保存二元運算符的AST,好比"+"。

class BinaryExprAST: ExprAST {
    
    let op: String
    
    let lhs: ExprAST
    
    let rhs: ExprAST
    
    init(_ op: String, _ lhs: ExprAST, _ rhs: ExprAST) {
        self.op = op
        self.lhs = lhs
        self.rhs = rhs
    }
    
}
複製代碼

由於這個類適用於二元運算符,因此AST須要記錄操做符左邊的AST(lhs)以及右邊的AST(rhs)以及操做符的名字。

函數的AST包括原型AST(PrototypeAST)、函數定義AST(FunctionAST)和函數調用AST(CallExprAST)。原型AST即函數的聲明。

class PrototypeAST {
    
    let name: String
    
    let args: [String]
    
    init(_ name: String, _ args: [String]) {
        self.name = name
        self.args = args
    }
    
}
複製代碼

PrototypeAST須要保存函數名以及參數名。

class FunctionAST {
    
    let proto: PrototypeAST
    
    let body: ExprAST
    
    init(_ proto: PrototypeAST, _ body: ExprAST) {
        self.proto = proto
        self.body = body
    }
    
}
複製代碼

FunctionAST保存函數聲明的AST proto和函數定義的AST body。

class CallExprAST: ExprAST {
    
    var callee: String?
    
    var args: [ExprAST]?
    
    init(_ callee: String, _ args: [ExprAST]) {
        self.callee = callee
        self.args = args
    }
    
}
複製代碼

CallExprAST用來解析函數的調用。

開始解析

在上一章中,咱們已經實現了一個能夠解析出token的lexer。下面咱們須要完善Lexer而且實現Parser,它用來解析出AST。

咱們爲Lexer添加輸入方法以及代理方法提供給Parser。

/// 解析代碼
    ///
    /// - Parameter sourceInput: 代碼數據源
    public func start(_ sourceInput: String) {
        let blockArray = sourceInput.split(separator: ";")
        for block in blockArray {
            source = Array(block + ";")
            index = 0
            lastChar = " "
            nextToken()
            switch currentToken!.token {
            case .def:
                delegate?.lexerWithDefinition(self)
                continue
            case .extern:
                delegate?.lexerWithExtern(self)
                continue
            case .number, .identifier:
                delegate?.lexerWithTopLevelExpression(self)
                continue
            default:
                continue
            }
        }
    }
複製代碼

由於Kaleidoscope用";"分割代碼塊,爲了方便處理咱們就能夠直接根據";"進行分塊解析便可。

接下來咱們定義Parser。

class Parser {
    
    private let lexer = Lexer()
    
    init() {
        lexer.delegate = self
    }
    
}

extension Parser {
    
    /// 解析代碼
    ///
    /// - Parameter sourceInput: 代碼數據源
    public func parse(_ sourceInput: String) {
        lexer.start(sourceInput)
    }
    
}
複製代碼

解析基本表達式

咱們從解析數字開始。

/// 解析數值常量
    ///
    /// - Returns: AST
    private func parseNumberExpr() -> ExprAST {
        let result = NumberExprAST(Double(lexer.currentToken!.val)!)
        lexer.nextToken()
        return result
    }
複製代碼

這個其實看代碼毫無疑問,生成NumberAST完獲取下一個token便可。

解析"'(' expression ')'"形式的表達式。

/// 解析'('開頭的表達式
    ///
    /// - Returns: AST
    func parseParenExpr() -> ExprAST? {
        lexer.nextToken()//跳過'('
        let v = parseExpression()
        guard v != nil else {
            return nil
        }
        if lexer.currentToken!.val != ")" {
            fatalError("Expected '\(lexer.currentToken!.val)'")
        }
        lexer.nextToken()//跳過')'
        return v
    }
複製代碼

parseExpression()方法將會在下面介紹到。

解析變量或者函數調用。

/// 解析變量引用和函數調用
    ///
    /// - Returns: AST
    private func parseIdentifierExpr() -> ExprAST? {
        let idName = lexer.currentToken!.val
        lexer.nextToken()
        if lexer.currentToken!.val != "(" {
          	//說明只是普通的變量
            return VariableExprAST(idName)
        }
      	//走到這說明是函數調用
        lexer.nextToken()
        var args: [ExprAST] = []
        if lexer.currentToken!.val != ")" {
          	//這個循環用來解析傳入參數
            while true {
                let arg = parseExpression()
                guard arg != nil else {
                    return nil
                }
                args.append(arg!)
              	//匹配到")"說明解析該結束了
                if lexer.currentToken!.val == ")" {
                    break
                }
              	//不一樣參數之間用","分割
                if lexer.currentToken!.val != "," {
                    fatalError("Expected ')' or ',' in argument list")
                }
                lexer.nextToken()
            }
        }
        
        lexer.nextToken()
        
        return CallExprAST(idName, args)
    }
複製代碼

如今咱們已經有了全部簡單表達式解析的邏輯了,咱們把它們的調用寫一個統一的入口。

/// 解析基本表達式的入口
    ///
    /// - Returns: AST
    private func parsePrimary() -> ExprAST? {
        guard lexer.currentToken != nil else {
            return nil
        }
        if lexer.currentToken!.val == "(" {
            return parseParenExpr()
        }
        switch lexer.currentToken!.token {
        case .identifier:
            return parseIdentifierExpr()
        case .number:
            return parseNumberExpr()
        default:
            fatalError("unknow token when expecting an expression")
        }
    }
複製代碼

解析二元表達式

首先咱們須要定義一個全局的操做符優先級表

var BinOpPrecedence: [String: UInt] = ["<": 10, "+": 20, "-": 20, "*": 40]
複製代碼

value越大表明優先級越大,很明顯"*"是大於"+"的,目前咱們只支持4個運算符,固然你本身能夠支持更多的運算符。

接着在Parser中定義得到操做符優先級的方法。

/// 獲取currentToken對應的運算符優先級
    ///
    /// - Returns: 優先級
    private func getTokenPrecedence() -> Int {
        if BinOpPrecedence[lexer.currentToken!.val] == nil {
            return -1
        } else {
            return Int(BinOpPrecedence[lexer.currentToken!.val]!)
        }
    }
複製代碼

接下來咱們須要實現parseExpression()方法。

/// 解析表達式
    ///
    /// - Returns: AST
    func parseExpression() -> ExprAST? {
        var lhs = parsePrimary()
        guard lhs != nil else {
            return nil
        }
        return parseBinOpRHS(0, &lhs!)
    }
複製代碼

運算優先級的解析思想是將二元運算符的表達式分爲多個部分。運算符優先級解析的基本思想就是經過拆解含有二元運算符的表達式來解決可能的二義性問題。以表達式a+b+(c+d)*e*f+g爲例,在進行運算符優先級解析時,它將被視做一串按二元運算符分隔的主表達式。按照這個思路,解析出來的第一個主表達式應該是a,緊跟着是若干個有序對,即:[+, b][+, (c+d)][*, e][*, f][+, g]。注意,括號表達式也是主表達式,因此在解析二元表達式時無須特殊照顧(c+d)這樣的嵌套表達式。

/// 解析二元運算符
    ///
    /// - Parameters:
    /// - exprPrec: 二元運算符優先級
    /// - lhs: 左表達式
    /// - Returns: AST
    private func parseBinOpRHS(_ exprPrec: Int, _ lhs: inout ExprAST) -> ExprAST? {
        while true {
            let tokPrec = getTokenPrecedence()
            if tokPrec < exprPrec {
                return lhs
            }
            
            //獲取二元運算符
            let binOp = lexer.currentToken
            lexer.nextToken()
            
            //解析二元運算符右邊的表達式
            var rhs = parsePrimary()
            guard rhs != nil else {
                return nil
            }
            
            let nextPrec = getTokenPrecedence()
            if tokPrec < nextPrec {
                //若是下一個符號優先級更高,則遞歸調用本身把它們拼成一個rhs返回
                rhs = parseBinOpRHS(tokPrec + 1, &rhs!)
                guard rhs != nil else {
                    return nil
                }
            }
          	//合併lhr和rhs
            lhs = BinaryExprAST(binOp!.val, lhs, rhs!)
        }
    }
複製代碼

解析其他結構

下面來解析函數原型。在Kaleidoscope中,有兩處會用到函數原型:一是extern函數聲明,二是函數定義。

/// 解析函數原型
    ///
    /// - Returns: 函數原型AST
    func parsePrototype() -> PrototypeAST {
        var fnName: String
        
        switch lexer.currentToken!.token {
        case .identifier:
            fnName = lexer.currentToken!.val
            lexer.nextToken()
            break
        default:
            fatalError("Expected function name in prototype.")
        }
        
        if lexer.currentToken!.val != "(" {
            fatalError("Expected '(' in prototype")
        }
        
        lexer.nextToken()
        var argNames: [String] = []
        while lexer.currentToken!.token == .identifier {
            argNames.append(lexer.currentToken!.val)
            lexer.nextToken()
        }
        if lexer.currentToken!.val != ")" {
            fatalError("Expected ')' in prototype")
        }
        lexer.nextToken()
        
        return PrototypeAST(fnName, argNames)
    }
複製代碼

解析函數定義就更簡單了,只須要先解析函數原型再解析表達式便可。

/// 解析函數定義
    ///
    /// - Returns: 函數定義AST
    private func parseDefinition() -> FunctionAST? {
        lexer.nextToken()
        let proto = parsePrototype()
        if let e = parseExpression() {
            return FunctionAST(proto, e)
        }
        return nil
    }
複製代碼

解析extern也很簡單,由於也是解析函數原型。

/// 解析extern導出定義
    ///
    /// - Returns: 原型AST
    private func parseExtern() -> PrototypeAST {
        lexer.nextToken()
        return parsePrototype()
    }
複製代碼

最後咱們還要容許用戶可以輸入任意表達式並求值,這個方式能夠經過添加一個特殊的匿名函數實現,這個函數不須要任何參數。

/// 解析頂級表達式
    ///
    /// - Returns: 函數AST
    private func parseTopLevelExpr() -> FunctionAST? {
        if let e = parseExpression() {
            //__anon_expr爲默認佔位函數名
            let proto = PrototypeAST("__anon_expr", [])
            return FunctionAST(proto, e)
        }
        return nil
    }
複製代碼

入口代碼

咱們還須要編寫一段輸入代碼可以讓咱們愉快的解析代碼。

首先咱們定義一種存放Kaleidoscope語言的文件,這裏就讓擴展名爲.k好了。這樣的話咱們須要先寫一個讀取文本內容的函數。

func readFile(_ path: String) -> String? {
    var path = path
    if path.hasSuffix("\n") {
        path.removeLast()
    }
    guard path.split(separator: ".").last! == "k" else {
        print("Expected file is *.k.")
        return nil
    }
    do {
        return try String(contentsOfFile: path, encoding: .utf8)
    } catch {
        print("Read file \(path) failure.")
        return nil
    }
}
複製代碼

接着咱們經過輸入的文件路徑讀這個文件的內容進行解析便可。

func main() {
    //解析器
    let parser = Parser()
    
    if let path = String(data: FileHandle.standardInput.availableData, encoding: .utf8) {
        if let str = readFile(path) {
            parser.parse(str)
        }
    }
}

main()
複製代碼
相關文章
相關標籤/搜索