本文向你們推薦一個yacc語法自動構建器,FSharpCompiler.Yacc
和FSharpCompiler.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上。