FSharpCompiler.Yacc 入門示例

本文向你們推薦一個yacc語法自動構建器,FSharpCompiler.YaccFSharpCompiler.Parsing前者是解析器生成工具,後者是解析器的依賴項。顧名思義,這個編譯器是專門爲F#語言使用的。這個文件位於https://github.com/xp44mm/FSharpCompiler.Docs/tree/master/FSharpCompiler.Docs你們有問題,能夠去提交issue,將會及時獲得更正或補充。git

龍書示例4.69:github

E -> E + T | T
T -> T * F | F
F -> ( E ) | digit

新建一個文本文件,輸入如下內容:正則表達式

line   : expr "\n"
       ;
expr   : expr "+" term
       | term
       ;
term   : term "*" factor
       | factor
       ;
factor : "(" expr ")"
       | DIGIT
       ;

這個文件能夠任何的擴展名,建議使用*.yacc做爲擴展名。表示這個文件是yacc語法規範文件。數據結構

說明:函數

語法符號說明:工具

語法符號字面量,爲雙引號包圍的字符串字面量。轉義規則同JSON語法。oop

語法符號不能夠爲空字符串"",空字符串被yacc內部用做特殊符號。測試

當語法符號匹配正則表達式\w+時,能夠省略包圍的引號,用標識符表示法。好比,"EXP"能夠簡寫爲EXP.this

空白僅用於分隔語法符號。.net

翻譯規則說明:

<head> : <body>_1
       | <body>_2
         ...
       | <body>_n
       ;

第一個產生式的左側符號是開始符號。

空白僅用於分隔語法符號。

無需輸入語義動做,語義動做已經內置爲構造語法樹。

新建一個FSharp xUnit測試項目:

安裝NuGet包:

Install-Package FSharpCompiler.Yacc
Install-Package FSharpCompiler.Parsing
Install-Package FSharp.xUnit
Install-Package FSharp.Literals
Install-Package FSharp.Idioms

前兩個包是編譯器的主包,其他的是各類經常使用功能的函數庫。本文爲節約篇幅,直接使用庫函數,而不展開代碼。

把上面輸入的規則文件添加到項目中,爲了方便。

將文件中的內容讀取到變量中:

let path = Path.Combine(__SOURCE_DIRECTORY__, @"E69.yacc")
let text = File.ReadAllText(path)

解析輸入文本:

let yaccFile = YaccFile.parse text
let yacc = ParseTable.create(yaccFile.mainRules, yaccFile.precedences)

生成數據:

[<Fact>]
    member this.``generate parse table``() =
        let result =
            [
                "let rules = " + Render.stringify yacc.rules
                "let kernelSymbols = " + Render.stringify yacc.kernelSymbols
                "let parsingTable = " + Render.stringify yacc.parsingTable
            ] |> String.concat System.Environment.NewLine
        output.WriteLine(result)

把輸出的結果,複製到一個單獨的模塊文件中好比:

module E69ParseTable

let rules = set [...]
let kernelSymbols = Map.ofList [...]
let parsingTable = set [...]

咱們可能常常升級改動輸入文件,這是驗證輸出文件和輸入文件是一致的方法:

[<Fact>]
    member this.``validate parse table``() =
        Should.equal yacc.rules E69ParseTable.rules
        Should.equal yacc.kernelSymbols E69ParseTable.kernelSymbols
        Should.equal yacc.parsingTable E69ParseTable.parsingTable

這裏須要添加NuGet程序包FSharp.xUnit,包裏面只有一個方法Should.equal x y

這就是yacc的基本用法。下一步,咱們將用生成的結果,構造一個解析器。

首先定義解析器的輸入,輸入爲一個F#可區分聯合的符記序列,咱們須要知道,語法分析其中那些符號是終結符號

[<Fact>]
    member this.``terminals``() =
        let grammar = Grammar.from yaccFile.mainRules
        let terminals = grammar.symbols - grammar.nonterminals
        let result = Render.stringify terminals
        output.WriteLine(result)

說明Grammar是yacc包中帶的工具類型,能夠獲取文法的經常使用性質,這裏是獲知文法中全部的終結符號集合。Render.stringify位於FSharp.Literals中,用於打印F#數據字面量,能夠理解爲數據源代碼。

打印的終結符號集合結果以下:

set ["\n";"(";")";"*";"+";"DIGIT"]

固然,有熟練的人能夠本身計算文法中的那些符號是終結符號。接下來,咱們根據此結果編寫Token類型:

type ExpToken =
    | EOL
    | LPAREN
    | RPAREN
    | STAR
    | PLUS
    | DIGIT of int

F#可區分聯合的標籤名首字母必須大寫,且是合法的標識,因此沒法作到Token的標籤和終結符號徹底相同。還須要定義一個成員,用於把Token數據映射到終結符號:

member this.getTag() =
        match this with
        | EOL -> "\n"
        | LPAREN -> "("
        | RPAREN -> ")"
        | STAR -> "*"
        | PLUS -> "+"
        | DIGIT _ -> "DIGIT"

定義了語法的輸入類型Token,這時就能夠寫解析器了。

let parser =
    SyntacticParser(
        E69ParseTable.rules,
        E69ParseTable.kernelSymbols,
        E69ParseTable.parsingTable)

let parseTokens tokens =
    parser.parse(tokens,fun (tok:ExpToken) -> tok.getTag())

SyntacticParser 位於FSharpCompiler.Parsing中的類型,其輸入正是前面咱們使用Yacc獲得的數據,對於一個文法,咱們只須要一個實例,應該把它的構造代碼放在解析器外面。parser.parse是生成抽象語法樹,第一個參數是詞法符記的序列,第二個參數用來向解析器提供終結符號,getTag去除了詞法符記的語義數據,只保留解析器所須要的文本數據,當樹節點解析成功時,根據終結符號在序列中的位置取出詞法符記的語義數據。綁定到抽象語法樹相應的節點上。

此時,咱們能夠測試這個函數:

[<Fact>]
    member this.``parse tokens``() =
        let tokens = [DIGIT 1;PLUS;DIGIT 2;STAR;LPAREN;DIGIT 4;PLUS;DIGIT 3;RPAREN;EOL]
        let tree = E69.Parser.parseTokens tokens
        let result = Render.stringify tree
        output.WriteLine(result)

能夠獲得結果:

let y = Interior("line",[
            Interior("expr",[
                Interior("expr",[
                    Interior("term",[
                        Interior("factor",[
                            Terminal(DIGIT 1)])])]);
                Terminal PLUS;
                Interior("term",[
                    Interior("term",[
                        Interior("factor",[
                            Terminal(DIGIT 2)])]);
                    Terminal STAR;
                    Interior("factor",[
                        Terminal LPAREN;
                        Interior("expr",[
                            Interior("expr",[
                                Interior("term",[Interior("factor",[Terminal(DIGIT 4)])])]);
                            Terminal PLUS;
                            Interior("term",[
                                Interior("factor",[Terminal(DIGIT 3)])])]);
                        Terminal RPAREN])])]);
            Terminal EOL])

返回一個抽象語法樹對象,這個樹用可區分聯合遞歸表達,此對象在FSharpCompiler.Parsing包中定義:

type ParseTree<'tok> =
    | Interior of symbol:string * children: list<ParseTree<'tok>>
    | Terminal of 'tok

這個數據結構是自解釋的,Interior是內部節點,非終結符號,Terminal是終結符號。

如何從普通文本獲得詞法符記序列,利用.net庫中的字符串和正則表達式方法,很容易就能夠寫一個詞法分析器,把它做爲詞法符記類型的靜態構造函數是很天然的:

static member from (text:string) =
        let rec loop (inp:string) =
            seq {
                match inp with
                | "" -> ()

                | Prefix @"[\s-[\n]]+" (_,rest) // 空白
                    -> yield! loop rest

                | Prefix @"\n" (_,rest) -> //換行
                    yield EOL
                    yield! loop rest

                | PrefixChar '(' rest ->
                    yield LPAREN
                    yield! loop rest

                | PrefixChar ')' rest ->
                    yield RPAREN
                    yield! loop rest

                | PrefixChar '*' rest ->
                    yield STAR
                    yield! loop rest

                | PrefixChar '+' rest ->
                    yield PLUS
                    yield! loop rest

                | Prefix @"\d+" (lexeme,rest) ->
                    yield  DIGIT(Int32.Parse lexeme)
                    yield! loop rest

                | never -> failwith never
            }
        loop text

這裏用到了活動模式,位於FSharp.Literals.StringUtils模塊中,須要安裝FSharp.Literals包。活動模式Prefix後面跟着一個正則表達式,匹配輸入字符串的最開頭的部分,則成功。輸入的正則表達式不須要使用^進行定位。活動模式PrefixChar 帶有一個字符參數,若是輸入字符串的首字符匹配,則成功。

測試這個函數:

[<Fact>]
    member this.``tokenize``() =
        let inp = "1+2*(4+3)" + System.Environment.NewLine
        let tokens = E69.ExpToken.from inp

        let result = Render.stringify (List.ofSeq tokens)
        output.WriteLine(result)

如上所屬最後兩行是打印輸出結果的功能。第一行是輸入,第二行獲得函數的輸出:

[DIGIT 1;PLUS;DIGIT 2;STAR;LPAREN;DIGIT 4;PLUS;DIGIT 3;RPAREN;EOL]

這個實例的源代碼在https://github.com/xp44mm/FSharpCompiler.Docs上。

相關文章
相關標籤/搜索