本示例是龍書4.9.2的示例,見圖4-59。
和前一章一樣,新建xUnit項目,用F#語言。起個名C4F59
安裝NuGet包:
Install-Package FSharpCompiler.Yacc
Install-Package FSharpCompiler.Parsing
Install-Package FSharp.xUnit
Install-Package FSharp.Literals
Install-Package FSharp.Idioms
編寫語法輸入文件C4F59.yacc
:
lines : lines expr "\n"
| lines "\n"
| /* empty */
;
expr : expr "+" expr
| expr "-" expr
| expr "*" expr
| expr "/" expr
| "(" expr ")"
| "-" expr %prec UMINUS
| NUMBER
;
%%
%left "+" "-"
%left "*" "/"
%right UMINUS
當需要指定運算符號的優先級時,文法輸入文件的結構為:
rule list
%%
precedence list
多行注釋同C語言語法/* .*? */
,不可以嵌套。將被忽略,不放入結果序列。
%prec UMINUS
用於為規則命名,這個名稱被優先級定義引用。
優先級規則同龍書中yacc的規則相同。這里暫且不深入分解。
文法的寫作並非一蹴而就,需要一些手段技巧編寫。此處,暫且直接輸入書中現成的文法,如何從零開始寫文法文件,將下一章詳細介紹。
輸入文件完成,我們可以解析輸入文件,得到結構化的數據。我們首先新建一個測試文件。然后將下面代碼放到一個測試中:
let path = Path.Combine(__SOURCE_DIRECTORY__, @"C4F59.yacc")
let text = File.ReadAllText(path)
let yaccFile = YaccFile.parse text
我們得到YaccFile的結構化數據,就是yaccFile
變量中。
let y = {
mainRules=[
["lines";"lines";"expr";"\n"];
["lines";"lines";"\n"];
["lines"];
["expr";"expr";"+";"expr"];
["expr";"expr";"-";"expr"];
["expr";"expr";"*";"expr"];
["expr";"expr";"/";"expr"];
["expr";"(";"expr";")"];
["expr";"-";"expr"];
["expr";"NUMBER"]
];
precedences=[
LeftAssoc,[TerminalKey "+";TerminalKey "-"];
LeftAssoc,[TerminalKey "*";TerminalKey "/"];
RightAssoc,[ProductionKey ["expr";"-";"expr"]]
]
}
這個結構化數據,排除了注釋和多余的空白,整理后,放入一個記錄中。注意輸入中的%prec
已經被消除,整理成等價的形式。F#是值相等,所以,盡管引用不相等,值相等的產生式仍然被當作一個數據。
此時我們可以打印解析表數據。
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 C4F59ParseTable
let rules = set [["";"lines"];["expr";"(";"expr";")"];....]
let kernelSymbols = Map.ofList [1,"lines";2,"(";3,"expr";....]
let parsingTable = set [0,"",-8;0,"\n",-8;0,"(",-8;....]
F#的文件是有依賴順序的,這個模塊應該在測試類之前添加。為了保證生成數據的完整性,添加一個驗證Fact:
[<Fact>]
member this.``validate parse table``() =
Should.equal yacc.rules C4F59ParseTable.rules
Should.equal yacc.kernelSymbols C4F59ParseTable.kernelSymbols
Should.equal yacc.parsingTable C4F59ParseTable.parsingTable
FSharpCompiler.Yacc
的主要任務生成語法解析表,到這里就完成了,下面介紹解析器的編寫。
定義文法的輸入類型:
首先,用如下方法,看看文法中用到了哪些個詞法符記:
[<Fact>]
member this.``terminals``() =
let grammar = Grammar.from yaccFile.mainRules
let terminals = grammar.symbols - grammar.nonterminals
let result = Render.stringify terminals
output.WriteLine(result)
輸出一個字符串集合:
set ["\n";"(";")";"*";"+";"-";"/";"NUMBER"]
很好,現在我們一對一,定義文法的輸入類型,詞法符記:
type C4F59Token =
| EOL
| LPAREN
| RPAREN
| STAR
| DIV
| PLUS
| MINUS
| NUMBER of int
member this.getTag() =
match this with
| EOL -> "\n"
| LPAREN -> "("
| RPAREN -> ")"
| STAR -> "*"
| DIV -> "/"
| PLUS -> "+"
| MINUS -> "-"
| NUMBER _ -> "NUMBER"
可區分聯合的getTag
成員,是文法輸入終結符號,字符串類型,與語義數據的獲取橋梁。
我們先不單元測試,我們先繼續完成詞法分析器,下面是將輸入變成詞法符記的程序:
open FSharp.Literals.StringUtils
open System
type ....
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 DIV
yield! loop rest
| PrefixChar '+' rest ->
yield PLUS
yield! loop rest
| PrefixChar '-' rest ->
yield MINUS
yield! loop rest
| Prefix @"\d+" (n,rest) ->
yield NUMBER(Int32.Parse n)
yield! loop rest
| never -> failwith never
}
loop text
詞法分析器利用兩個活動模式Prefix
,和PrefixChar
。Prefix
檢測輸入的頭部是否匹配給定的正則表達式,如果匹配,將字符串分為兩部分,頭部的匹配的子字符串,和剩余部分的字符串。如:
| Prefix @"\d+" (n,rest) ->
會成功匹配字符串"123xyz..."
。並返回元組為"123"
,"xyz..."
前者賦值給n
,后者賦值給rest
。
PrefixChar
檢測輸入的第一個字符是否是給定的字符,如果是,將返回除去頭部字符的剩余部分的字符串。如:
| PrefixChar '-' rest ->
會成功匹配字符串"-123"
。並返回給參數rest
為"123"
。
測試詞法分析器:
[<Fact>]
member this.``tokenize``() =
let inp = "-1/2+3*(4-5)" + System.Environment.NewLine
let tokens = C4F59Token.from inp
let result = Render.stringify (List.ofSeq tokens)
output.WriteLine(result)
得到結果:
[MINUS;NUMBER 1;DIV;NUMBER 2;PLUS;NUMBER 3;STAR;LPAREN;NUMBER 4;MINUS;NUMBER 5;RPAREN;EOL]
有了解析表數據,我們編寫解析器代碼:
module C4F59.C4F59Parser
open FSharpCompiler.Parsing
let parser =
SyntacticParser(
C4F59ParseTable.rules,
C4F59ParseTable.kernelSymbols,
C4F59ParseTable.parsingTable
)
let parseTokens tokens =
parser.parse(tokens,fun (tok:C4F59Token) -> tok.getTag())
我們首先打開名字空間FSharpCompiler.Parsing
,同名NuGet包,利用SyntacticParser
類型構造解析器,解析器是單例的,只需要初始化構造一次即可。解析方法的第一個參數是詞法符記的序列,第二個參數是一個函數,用來告訴解析方法如何獲得語義數據類型的標簽字符串,作為文法的終結符號。
我們測試這個方法:
[<Fact>]
member this.``parse tokens``() =
let tokens = [MINUS;NUMBER 1;DIV;NUMBER 2;PLUS;NUMBER 3;STAR;LPAREN;NUMBER 4;MINUS;NUMBER 5;RPAREN;EOL]
let tree = C4F59Parser. parseTokens tokens
let result = Render.stringify tree
output.WriteLine(result)
輸出結果如下:
let y = Interior("lines",[
Interior("lines",[]);
Interior("expr",[
Interior("expr",[
Interior("expr",[Terminal MINUS;Interior("expr",[Terminal(NUMBER 1)])]);
Terminal DIV;
Interior("expr",[Terminal(NUMBER 2)])]);
Terminal PLUS;
Interior("expr",[
Interior("expr",[Terminal(NUMBER 3)]);
Terminal STAR;
Interior("expr",[
Terminal LPAREN;
Interior("expr",[
Interior("expr",[Terminal(NUMBER 4)]);
Terminal MINUS;
Interior("expr",[Terminal(NUMBER 5)])]);
Terminal RPAREN])])]);
Terminal EOL])
數據已經整理成為樹形,但是這個類型過於通用,我們可以遍歷樹,根據樹上面的數據轉換為更專用的數據。我們定義一個表達式數據類型。
type C4F59Expr =
| Add of C4F59Expr * C4F59Expr
| Sub of C4F59Expr * C4F59Expr
| Mul of C4F59Expr * C4F59Expr
| Div of C4F59Expr * C4F59Expr
| Negative of C4F59Expr
| Number of int
下面是轉換模塊:
module C4F59.C4F59Translation
open FSharpCompiler.Parsing
///
let rec translateExpr = function
| Interior("expr",[e1;Terminal PLUS;e2;]) ->
C4F59Expr.Add(translateExpr e1, translateExpr e2)
| Interior("expr",[e1;Terminal MINUS;e2;]) ->
C4F59Expr.Sub(translateExpr e1, translateExpr e2)
| Interior("expr",[e1;Terminal STAR;e2;]) ->
C4F59Expr.Mul(translateExpr e1, translateExpr e2)
| Interior("expr",[e1;Terminal DIV;e2;]) ->
C4F59Expr.Div(translateExpr e1, translateExpr e2)
| Interior("expr",[Terminal LPAREN;e;Terminal RPAREN;]) ->
translateExpr e
| Interior("expr",[Terminal MINUS;e;]) ->
C4F59Expr.Negative(translateExpr e)
| Interior("expr",[Terminal (NUMBER n);]) ->
C4F59Expr.Number n
| never -> failwithf "%A" never.firstLevel
///
let rec translateLines tree =
[
match tree with
| Interior("lines",[lines;expr;Terminal EOL]) ->
yield! translateLines lines
yield translateExpr expr
| Interior("lines",[lines;Terminal EOL]) ->
yield! (translateLines lines)
| Interior("lines",[]) ->
()
| _ -> failwithf "%A" tree.firstLevel
]
這里函數的輸入參數類型是ParseTree
類型,此類型位於FSharpCompiler.Parsing
中,所以先打開名字空間。這個轉譯函數對應yacc輸入文件的文法,每個函數對應一組產生式,依賴最少的非終結符號先定義。對於每個函數的每個匹配項對應一個產生式,匹配項一定是形如:
| Interior("left side",[symbol1;symbol2;....]) ->
文法的產生式一定對應節點Interior
,節點的標簽一定是產生式左側的那個非終結符號,節點的子節點依次對應產生式右側的元素。個數是相等的,空產生式對應樹節點子節點列表也是空列表。如果子節點為終結符號則,如果字節的為非終結符號,定義一個值,以遞歸調用翻譯函數。如果是終結符號,則顯式列出,以匹配同一文法符號的不同產生式的特征。
| Interior("lines",[lines;expr;Terminal EOL]) ->
yield! (translateLines lines)
yield expr
對應產生式:
lines : lines expr "\n"
產生式對應Interior
;"lines"
對應左側的文法符號;子節點列表,[lines;expr;Terminal EOL]
對應產生式: lines expr "\n"
;其中lines
對應lines
;expr
對應expr
;Terminal EOL
對應"\n"
。
非終結符號對應的子樹將會被遞歸翻譯。如果確定無用,也可能被直接丟棄。終結符號對應的子樹將會被提取有用的數據被轉化成新的數據形式,或無用的數據被丟棄。
每組產生式的最后有一個默認的后備匹配項目,正確的程序永遠用不到,如果用到只會用到輸入樹第一層的子樹數據,即可以確定錯誤:
| _ -> failwithf "%A" tree.firstLevel
測試:
[<Fact>]
member this.``translate to expr``() =
let tokens = [MINUS;NUMBER 1;DIV;NUMBER 2;PLUS;NUMBER 3;STAR;LPAREN;NUMBER 4;MINUS;NUMBER 5;RPAREN;EOL]
let tree = C4F59Parser. parseTokens tokens
let exprs = C4F59Translation.translateLines tree
let result = Render.stringify exprs
output.WriteLine(result)
輸出結果:
[Add(Div(Negative(Number 1),Number 2),Mul(Number 3,Sub(Number 4,Number 5)))]
是不是清爽多了。
本章介紹了如何編譯帶優先級的文法,這個優先級已經是別人調試正確的文法。下一章將介紹如何從零寫優先級。