BNF范式含義和基本用法


什么是BNF?       Backus-Naur符號(就是眾所周知的BNF或Backus-Naur Form)是描述語言的形式化的數學方法,由John Backus (也許是Peter Naur)開發,用於描述Algol 60編程語言的語法。 最初的時候是許多標記(圖例),是John Backus 在數學家Emil Post早期工作的基礎上開發的,Peter Naur 在Algol 60中采用了它,並進行了稍許改進,從而使其出名,因此Naur把BNF叫做Backus Normal Form,而其他人則把它叫成Backus-Naur Form。 BNF被用來形式化定義語言的語法,以使其規則沒有歧義。事實上,BNF非常精確,圍繞這些語法有很多數學理論,使得人們竟然可以機械地為基於BNF語法的語言構造解析器。(有些語法不能實現,但通常可以手工地通過轉換成其他形式來實現)。 實現這個功能的程序叫做編譯器,最有名的是YACC,當然,還有很多很多。 工作原理 基本原理 BNF類似一種數學游戲:從一個符號開始(叫做起始標志,實例中常用S表示),然后給出替換前面符號的規則。BNF語法定義的語言只不過是一個字符串集合,你可以按照下述規則書寫,這些規則叫做書寫規范(生產式規則),形式如下: symbol := alternative1 | alternative2 ... 每條規則申明:=左側的符號必須被右側的某一個可選項代替。替換項用“|”分割(有時用“::=”替換“:=”,但意思是一樣的)。替換項通常有兩個符號 和終結符構成。之所以叫做終結符是因為沒有針對他們的書寫規范,他們是書寫過程的終止(符號通常被叫做非終止符,也有人叫非終端)。 BNF語法的另一個變化是把終止符(終端)放在引號中,把他們與符號區別開來。有些BNF語法用符號明確地標明允許使用空格的地方,而有的語法則把它留給讀者推測。 BNF中有一個特殊符號“@”,表示符號可以去掉。如果用@替換符號,只需要將符號去掉。這非常有用,因為如果不利用這個技巧,有時很難終止替換過程。 因此,一個語法描述的語言就是用書寫規則(生產式規則)寫的字符串的集合。如果一個字符串無法用這些規則寫出,那么,該字符串在這個語言中就被禁用。 一個實例 下面是BNF語法的一個實例: S      := '-' FN |      FN FN := DL |      DL '.' DL DL := D | D DL D      := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 這里的各種符號都是縮寫的:S是開始符,FN產生一個分數,DL是一串數字列表,D是各個數字。 該語法描述的語言的有效句子都是數字,可能是分數,也可能是負數。要寫一個數字,首先以S符號開頭: S 然后,用S的結果替換它。上例中,我們可以選擇在數字前面不加“-”號而僅僅使用FN,並用FN替換S: FN 接着用FN的結果替換FN。我們想寫一個分數,所以選擇產生兩個中間帶“.”的十進制數的列表,然后,我們接着在每一行用一個符號的結果替換一個符號,像下面的例子: DL . DL D . DL 3 . DL 3 . D DL 3 . D D 3 . 1 D 3 . 1 4 這里我們寫了一個分數3.14。如何寫-5,讀者可自己練習。要徹底搞明白,還需要研究語法,知道您弄明白為什么按照上述規則不能寫出3..14。 EBNF及其用途 在DL中,我們已經用到了遞歸(如DL能產生新的DL)來表達許多數字D的情況。這有點不太靈活,使BNF難以閱讀。擴展BNF(EBNF)通過引入下列操作符解決了這個問題: l ?:意思是操作符左邊的符號(或括號中的一組符號)是可選項(可以出現0到多次)。 l *:是指可以重復多次。 l +:是指可以出現多次。 一個EBNF語法實例 上面的例子用EBNF可以寫成: S := '-'? D+ ('.' D+)? D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 提示:EBNF在定義語言方面並不比BNF更強大,只是更方便。凡是用EBNF寫的東西都可以轉換成BNF的形式。 BNF和EBNF的使用 一般用法       多數編程語言標准都使用一些EBNF變量來定義語言的語法。這有兩個好處:一是在語言的語法上沒有爭議,而是易於編譯器的編寫,因為編譯器的解析器可以用類似YACC這樣的“編譯器編譯器”自動產生。 EBNF也用於許多其他標准,如定義協議格式,數據格式和XML,SGML這樣的標記語言。(HTML沒有按照語法定義,而是用SGML DTD這樣的高級語法定義的。) 在BNF web club( http://cuiwww.unige.ch/db-research/Enseignement/analyseinfo/BNFweb.html )上可以查到BNF的一個語法集。 如何使用形式語法 現在,我們已經了解什么是BNF和EBNF以及它們的用途了,但還不知道為什么它們很有用以及如何利用它們。 形式語法最明顯的用法已經提到過了:一旦為語言制定了形式語法,就完全定義了這個語言。對於那些東西可以用於語言,那些東西不可以就不會有歧義了。這非常有用因為用普通語言描述的語法不僅冗長,而且會產生對不同的解釋。 另一個好處是:形式語法是數學產物,可以被計算機理解。實際上有許多程序可以用(E)BNF語法輸入,並自動為給定語法的解析器提供處理代碼。實際上,這是設計編譯器的常見做法:用帶有語法輸入的所謂“編譯器編譯器”產生編程語言的解析器代碼。 當然,編譯器除了做語法檢查之外還做許多其他檢查,他們也產生代碼。這些都沒有在(E)BNF中描述,因此編譯器編譯器通常有針對一種語法中不同代碼的代碼塊連接(也叫做操作)的特殊語法。 最有名的編譯器編譯器是YACC(Yet Another Complier Complier),產生C代碼,其他的編譯器還有針對C++,JAVA,Python等等其他語言的。 解析 最簡單的方法 自上而下的解析(LL)       按照現有語法解析代碼的最簡單方法是叫做LL解析(自上而下解析)。它是這樣工作的:對每一段代碼找出代碼開始的非終端標記(叫做初始集)。 然后,在解析時只需要從起始符開始比較不同代碼段(符號)初始集和第一個輸入的符號,判斷代碼段用的是哪個起始符作為開始的(用到了那些符號)。只有不存 在一個符號的兩個初始集含有相同的終止符時才可以這樣。否則,就不能通過輸入的第一個終止符選擇哪個開始符(符號)了。 LL語法通常按數字分類,比如LL(1), LL(0)等等。括號中的數字是在語法中的任何一點選擇適當符號時需要同時考慮的最大終端數。因此 LL(0)根本不需要檢查終端(終止符),總能夠選擇適當的終止符。這種情況只發生在所有符號只有一個替換符的情形,而如果只有一個替換符意味着語言只有 一個字符串。也就是說LL(0)沒有意義。 最常有也是做有用的LL語法是聯絡LL(1),通過檢查輸入的第一個終止符總能夠選擇適當的替換符。而LL(2)需要檢查兩個符號,以此類推。 一個LL分析實例 為了說明,我們就實例語法做一個初始集分析。對符號D這非常容易:所有的替換符號跟它們的初始集(它們產生的)一樣是一個數字,D符號把由10個數字組成的集合作為初始集。這意味着至少有一個LL(1)語法,因為這時我們需要一個終端來選擇適當的替換符。 對DL就會有些麻煩。兩個替換符都以D開頭,因此都有相同的初始機。這意味着不能夠通過檢查輸入的第一個終止符來選擇適當的替換符。但我們可以通過欺騙輕 松地克服這個困難:如果輸入的第二個終止符不是數字,我們就用第一個替換符,如果兩個都是數字,就用第二個替換符。也就是說,這至少是LL(2)語法。 這里其實把事情簡化了。DL的替換符並沒有告訴我們D @的替換符中的第一個終止符后哪個終止符是被允許的,因為我們需要知道在DL后面哪個終止符被允許。這個終止符集叫做符號的后續集(follow set of the symbol),這里是“.”,是輸入的結尾。 FN符號更糟糕,因為兩個替換符都是以數字作為其初始集。檢查第二個也終止符沒有用,因為在數字列表(DL)中的最后一個數字的后面需要查看第一個終止 符,而我們需要讀取所有的數字后才能知道有多少數字。由於有多少數字並無限制,這就不是一個對於任意k的LL(k)語法。 或許有些奇怪,S符號很簡單。第一個替換項“-”是它的初始集,第二個全部是數字。也就是說,解析時從S符號開始查看輸入項來決定使用哪個替換項。如果第一個終端是“-”,就用第一個替換項“-”;否則就用第二個。只有FN和DL替換想會引起麻煩。 一個LL轉換實例 其實也沒有必要失望。多數非LL(k)語法可以容易地轉換成了LL(1)語法。這樣我們需要轉換兩個符號:FN和DL。 FN的問題是兩個替換項都以DL開頭,但第二個后接一個“.”和另外一個初始DL后面的DL。這很好解決:我們把FN變成只有以一個DL開頭的替換項后跟一個FP(分數部分),FP可以是空或是”.”后跟一個DL。變成下面這樣: FN := DL FP FP := @ | '.' DL 現在FN沒有問題了,因為只有一個替換項,FP也沒有問題,因為兩個替換項有不同的初設集。分別是輸入的結尾和“.”。 DL是塊硬骨頭,因為主要問題出在遞歸,而且是復合的,需要從DL中得到一個D。解決方案是給DL一個單獨的替換項,一個D后接一個DR(其余的數字)。 這時DR就有兩個替換項:D和DR或@。第一個替換項以所有的數字為初始集,第二個含有“.”和輸入的結尾作為其初始集,從而解決問題。 這樣就徹底轉換成LL(1)語法了: S      := '-' FN | FN FN := DL FP FP := @ | '.' DL DL := D DR DR := D DR | @ D      := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 稍難的方法 自底而上的解析(LR) 一個稍難的方法是移動替換法或叫自底而上解析。這個技術采集所有輸入直道發現能夠還原成符號的輸入。這聽起來好像很困難,所以需要給個例子加以說明。我們解析“3.14”看看如何從該語法中產生。我們從讀取輸入“3”開始: 3 然后,我們查看能否還原成產生它的符號。實際上我們能,它是從符號D產生的,我們用D替換3。這時我們注意到D可以從DL產生,所以用DL替換D。(這個 語法有不確定性,我們可以一直還原到FN,但是是錯誤的。簡單起見我們這里跳過錯誤的步驟,但清晰的語法將不會允許這樣的錯誤發生),之后我們從輸入中讀 取“.”並試圖還原它,但是失敗了。 D DL DL . 他不能還原成其它的東西,所以我們繼續讀取其它輸入“1”,並把它還原成D,在讀取下一個輸入“4”, “4”也可以還原成D,再還原成DL,然后,“D DL”序列可以進一步還原成DL。 DL . DL . 1 DL . D DL . D 4 DL . D D DL . D DL DL . DL 看一下語法就會發現FN恰好能夠還原“DL.DL”序列,於是將其還原。FN是從S產生的,所以FN可以還原成S,到此為止就完成了解析。 DL . DL FN S 也許你注意到我們可以先在做還原,也可以等到有多個符號在做不同的還原。這種移位還原解析算法有許多復雜變化,按照復雜程度和功能分為LR(0)、 SLR、LALR和LR(1)。LR(1)需要太大的解析表,所以LALR是最常用的算法,而SR(0)和SLR對多數編成語言都不夠強大。 LALR 和LR(1)實在太復雜了,這里沒法討論,但你已經了解了其基本思想。 LL還是LR? 這個問題已經被人回答了,這里引用他的新消息: 希望不要引發爭議,首先,當Frank看到這個時不要打我(我老板就是Frank DeRmer,LALR解析的創始人…)。 (借用一下Fischer&LeBlanc's "Crafting a Compiler"的摘要) 簡單性Simplicity        - - LL 通用性Generality        - - LALR 操作Actions           - - LL 錯誤恢復Error repair      - - LL 表大小Table sizes       - - LL 解析速度Parsing speed - - comparable (me: and tool-dependent) 簡單性- - LL 占優 ========== LL解析器更簡單,如果要調試一個解析器,考慮遞歸下降解析器(編寫LL解析器的常用方法)要比LALR解析器的表簡單多了。 通用性 - - LALR 占優 ========== 在規范定義的簡易性,LALR輕松取勝。LL和LALR的最大不同是LL語法必須用左因子法則和消除左遞歸。 提取左因子是必須的,因為LL解析器需要基於固定的輸入數選擇替換。 左回歸是有問題的,因為規則前導標記總是統一規則的前導標記。這將導致無窮遞歸。 參考以下連接了解LALR轉換成LL語法的方法: http://www.jguru.com/thetick/articles/lalrtoll.html 許多語言已經有了LALR語法,但還需要翻譯。如果語言沒有這樣的語法,寫一個LL語法也不是什么難事。 操作性 - - LL 占優 ======= 在LL解析器中可以把操作放在任何地方而不會引起沖突。 錯誤修復 - - LL 占優 ============ LL解析器具有豐富的環境信息(上下文信息),對修復錯誤很有幫助而非報告所無。 表大小 - - LL =========== 假設你編寫一個表驅動LL解析器,它的表只有其他的一半大小。(平心而論,有很多方法優化LALR表,使其變小) 解析速度 – 比較(我的觀點:視工具而定) --Scott Stanchfield in article < 33C1BDB9.FC6D86D3@scruz.net > on comp.lang.java.softwaretools Mon, 07 Jul 1997. 更多信息 John Aycock用Python開發了一個非常好而且簡單易用的解析框架叫做SPARK ,用可讀性非常強的論文描述。 編譯器和解析器方面權威工作是'The Dragon Book',也叫Compilers : Principles, Techniques, and Tools,作者Aho, Sethi and Ullman。請注意這是一本高級的數學書。 在線的免費資料較好的是這本: http://www.cs.vu.nl/~dick/PTAPG.html 。 Frank Boumphrey的another EBNF tutorial,( http://www.hypermedic.com/style/xml/ebnf.htm ) Henry Baker的an article about parsing in Common Lisp( http://home.pipeline.com/~hbaker1/Prag-Parse.html       )呈現的是一個簡單、高效而且十分方便的解析框架。方法類似於編譯器編譯器,但它依賴於一個十分強大的Common Lisp宏系統。 定義BNF語法的句法規則在RFC 2234( http://www.ietf.org/rfc/rfc2234.txt )中可以找到,the ISO 14977 standard中也有。 author:peiseny


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM