前言
在日常的開發工作中我們總是時不時需要寫一些語法分析器。語法分析器不一定指的是一門語言的編譯器前端,也有可能僅僅是一個自己設計格式的配置文件的讀寫程序,或者是一門用來簡化我們開發的DSL(領域專用語言)。我們可以選擇使用XML,不過因為XML的噪音實在是太多,所以自己寫語法分析器在有些情況下是必要的,特別是那種經常需要修改的文件,使用XML有時候會增加我們的負擔,除非我們專門為此開發一個編輯器程序。
這篇文章將緊密結合一個帶函數的四則運算計算器的例子(DocumentationSamplesExpressionCalculatorExpressionCalculator.sln)來說明如何使用Vczh Library++提供的工具來大幅度簡化我們的語法分析器的開發,並最終給出一個可以編譯的例子。雖然這個例子實在是老掉牙了,不過開發一個四則運算計算器可以覆蓋大部分開發語法分析的過程中會遇到的問題,所以也不失為一個好的例子。
這個例子可以在Vczh Library++的代碼裡面找到。
制定語法
我們需要對帶函數的四則運算計算器下一個定義,這樣我們才可以有目的地完成這個任務。我們對四則運算式子是很熟悉的,一個四則運算式子包含加減乘除、括號和數字。我們還可以支持負號:-a,其實是(0-a)的簡寫形式。那麼什麼是支持函數呢?這裡我們只考慮單參數函數的情況,譬如說三角函數和對數指數等等。譬如說下面的式子就是滿足定義的帶函數的四則運算式子:
sin(1+2) + cos(3*-4)
Vczh Library++使用語法的角度來對待一個字符串,因此我們可以把上面的定義轉換成語法。一個語法用來表示字符串的一個子集。我們可以通過語法來表達什麼樣的字符串是滿足規定的,什麼樣的字符串是不滿足規定的。不過一個具有現實意義的語法總是會有一些局限性的,譬如說你很難用上下文無關的文法來表達一個字符串:a…ab…bc…c,其中三種字母的數量都相等。幸好在絕大多數情況下我們都不需要去面對這些高難度的問題,因此可以用一些簡單的規則來處理:
RULE = EXPRESSION
RULE是這個規則的名字,而EXPRESSION是這個規則的定義。語法可以由一條規則組成,也可以由很多條規則組成。當所有的規則都列出來之後,那麼每一個規則的名字都是一個字符串的集合。大部分情況下你需要指定一個“總入口”來代表整個語法。
舉個例子,假設我們判斷一個字符串是不是無符號整數。一個無符號整數只能由數字字符組成。於是我們可以先用一條規則來代表“數字字符”。這裡我們可以使用“|”來代表“或”,那麼下面的規則就表示DIGIT是’0’或’1’或…或’9’:
DIGIT = ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’
那麼,無符號整數就是“很多數字字符”:
INTEGER = DIGIT | INTEGER DIGIT
無符號整數INTEGER要麼是一個數字字符,要麼就是一個合法的無符號整數後面再加上一個數字字符。無符號整數加上一個數字字符仍然是一個無符號整數。
現在可以來檢驗一下。譬如說“1”是一個無符號整數,那麼從INTEGER開始,分析“1”所走的路徑就是
INTEGER
= DIGIT (INTEGER = DIGIT)
= ‘1’ (DIGIT = ‘1’)
字符串“123”顯然也應該是一個無符號整數。“123”是一些數字字符組成的,因此走的路徑跟單個字符稍微有些不同。這裡將會交替使用INTEGER的兩條路徑來模擬循環:
INTEGER
= INTEGER DIGIT (INTEGER = INTEGER DIGIT)
= INTEGER DIGIT DIGIT (INTEGER = INTEGER DIGIT)
= DIGIT DIGIT DIGIT (INTEGER = DIGIT)
= ‘1’ DIGIT DIGIT (DIGIT = ‘1’)
= ‘1’ ‘2’ DIGIT (DIGIT = ‘2’)
= ‘1’ ‘2’ ‘3’ (DIGIT = ‘3’)
在使用INTEGER分析“123”的時候,我們可以交替使用INTEGER = DIGIT和INTEGER = INTEGER DIGIT這兩條規則來將一個INTEGER替換成恰好三個DIGIT,然後再將DIGIT替換成’1’、’2’和’3’三個字符,從而確信“123”滿足INTEGER的定義,因此“123”是一個無符號整數。
替換的過程並不是唯一的,我們完全可以使用另一種順序來將INTEGER替換成“123”:
INTEGER
= INTEGER DIGIT (INTEGER = INTEGER DIGIT)
= INTEGER ‘3’ (DIGIT = ‘3’)
= INTEGER DIGIT ‘3’ (INTEGER = INTEGER DIGIT)
= INTEGER ‘2’ ‘3’ (DIGIT = ‘2’)
= DIGIT ‘2’ ‘3’ (INTEGER = DIGIT)
= ‘1’ ‘2’ ‘3’ (DIGIT = ‘1’)
這正是語法的一個特點:替換順序與結果無關。
現在我們將這個例子再深入一點,如何用語法規則來描述一個逗號分隔的無符號整數列表呢?逗號分隔的無符號整數列表可以是一個整數“123”,也可以使多個整數“1,23,456”。這也是重復的一種,只是跟INTEGER的那種重復有所區別——多了一個逗號。根據上面的描述可以知道,逗號分隔的無符號整數列表有兩種情況,第一種是單獨的一個整數,第二種是一個已經完成的列表後面跟著一個逗號和一個整數。那麼事情就變得簡單了。假設我們使用LIST來代表這個列表,那麼根據上面的描述我們可以用類似的技巧來描述它:
LIST = INTEGER | LIST ‘,’ INTEGER
用LIST來分析一個數字列表的過程與用INTEGER分析一個無符號整數是相似的。因為篇幅問題,這裡只展示使用LIST處理“1,23,456”的其中一種方法:
LIST
= LIST ‘,’ INTEGER (LIST = LIST ‘,’ INTEGER)
= LIST ‘,’ INTEGER ‘,’ INTEGER (LIST = LIST ‘,’ INTEGER)
= INTEGER ‘,’ INTEGER ‘,’ INTEGER (LIST = INTEGER)
= DIGIT ‘,’ INTEGER ‘,’ INTEGER (INTEGER = DIGIT)
= ‘1’ ‘,’ INTEGER