Yacc使用優先順序
本示例是龍書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)))]
是不是清爽多了。
本章介紹瞭如何編譯帶優先順序的文法,這個優先順序已經是別人除錯正確的文法。下一章將介紹如何從零寫優先順序。