CFG 設計(CFG design)
設計上下文無關文法就像為一整族字串寫一份遞迴食譜:你直接描述最簡單的情形,再描述如何用較小的情形建造較大的情形。這種心態就是遞迴思考——「一個平衡字串要麼是空的,要麼是一個較小的平衡字串外包一對配對符號,要麼是兩個平衡字串並排」。把每個這樣的句子變成一條產生式,你就有了一個文法。
實務上,你的做法是辨識語言的遞迴形狀,並給每個有意義的子範疇各自的非終端符號。有幾個模式出現得太頻繁,值得背下來。對配對計數 a^n b^n:S → a S b | ε——每條規則在左側加一個 a、右側加一個 b,保持兩者相等。對平衡括號:S → ( S ) | S S | ε。對 {a, b} 上的回文:S → a S a | b S b | a | b | ε——對稱的規則從兩端生長字串。對簡單的敘述區塊:Block → { StmtList }、StmtList → Stmt StmtList | ε。遞迴規則(一個重新出現在自己右側的非終端符號)正是讓區區數條規則得以描述無窮多、愈來愈深的字串的關鍵。
對於像算術這樣的東西,你通常想要的不只是單純生成——你想要文法「編碼」意義,使其剖析樹依數學的方式把運算元分組。這意味著用分層的非終端符號把優先序與結合性烤進去(Expr 管 + 與 -、Term 管 * 與 /、Factor 管原子與括號),這也讓文法變得無歧義。所以好的 CFG 設計同時有兩個目標:生成恰好正確的字串集合,「並且」給每個字串一棵反映其預期結構的剖析樹。初學者常犯的錯誤是寫出一個生成正確語言、卻有歧義的文法——作為集合是對的,作為意義的描述卻是錯的。
帶優先序與左結合性的無歧義算術:Expr → Expr + Term | Term;Term → Term * Factor | Factor;Factor → ( Expr ) | id。這迫使 a + a * a 分組為 a + (a * a),編碼了 * 比 + 結合得更緊。
遞迴地思考:基底情形 + 一條由小建大的規則。分層使用非終端符號以編碼優先序。
生成正確的字串集合只是工作的一半;一個無歧義、又能給每個字串其「預期」結構的文法,才是真正的目標。一個正確但歧義的文法是初學者常見的陷阱。