語義分析之一:屬性文法
編譯原理的幾個核心階段:詞法分析、語法分析和語義分析,其實編譯的本質便是翻譯,其各個階段便是承擔不同的翻譯任務,詞法分析階段的任務是將程式輸入的字串流翻譯成語言認可的字元流(剔除空格和註釋等部分);語法分析便是將程式按照語言文法的規則構建成語法樹;語義分析便是在語法樹構建的基礎上完成語言規則的語義動作(型別檢查、作用域和可視性檢查、一致性檢查等)。
現今的程式語言絕大多數都是圖靈完備的,故而語法設計上幾乎很難出現決定性的差異,更多的語言特徵區別便是在語義動作上設計的不同所帶來的效果。其實語言的諸多優美特徵都是在語義階段新增進去的,比如C語言規定變數宣告部分必須在操作部分之前全部完成,而C++則可以在程式中隨時宣告變數,比如一些隱藏的型別轉換都是在語義分析階段完成的,所以某種意義上,語義分析也是語言設計和程式程式設計間的“銀彈”。所以相比於語法階段的樹擴充套件或圖搜尋之類的圖論知識的枯燥,語義分析階段可以看到很多和語言特性鮮活的對應關係。
我們知道,對於語言而言,無論變數、函式、過程在程式中都是用一個識別符號來代替,但如果給定了一個識別符號,我們如何確定這個識別符號的意義呢?其實這便引匯出屬性文法的概念(其實語義分析的公式化有多種方式,比如操作語義學、公理語義學、屬性文法等,其中屬性文法最為直觀,也是當前絕大多數編譯器採用的編譯方式),比如變數有int\float\double之類的區別,那顯然給定一個變數識別符號,必須要指明該識別符號的“資料型別屬性”,所以必須給所有識別符號配備一系列的屬性。利用識別符號的這些屬性,便可以用來配合此前構建的語法樹進行一系列的語義動作(型別檢查、可見性是否合法等)。
語義分析一般是和語法分析組合在一起執行的,語法分析完成前一步語法樹分析的構建(呼叫某個產生式完成一步規約,形成當前的樹節點
形式上講,一個屬性文法是一個三元組,A=(G,V,F),一個上下文無關文法G;一個屬性的有窮集V和關於屬性的謂詞函式的有窮集F。每個斷言與文法的某產生式相聯。如果對G中的某一輸入串而言(句子),A中的所有斷言對該輸入串的語法樹結點的屬性全為真,則該串也是A語言中的句子,如下便是一個關於屬性文法的例子
1. E→T1+T2 {T1.t=int AND T2.t=int}//謂詞,要求若是符合該產生式,則進行相加的兩元素必須都是整型
2. E→T1orT2 {T1.t=bool AND T2.t=bool}
3. T→num {T.t∶=int} //屬性加工,宣告該變數為整型,該識別符號資料型別屬性被賦值為“整型”
4. T→true {T.t∶=bool}
5. T→false {T.t∶=bool}
歸根而言,屬性文法便是為所有識別符號(無論是否是終結符)配備一些屬性的文法,這些屬性不僅可以描述設計語言的語法有效補充,又可以為語義分析提供足夠的資料支援。在推導語法樹的過程中,識別符號的屬性值也在不斷加工並通過賦值規則不斷層層傳遞。
既然介紹完了屬性文法的屬性重要性,那麼如何利用這些屬性,便是語義分析的重要所在。下面便摘錄《編譯原理》一書中的幾個例子來演示屬性文法的語義處理加工。
1. 賦值語句的語義翻譯
(1) S→id∶=E {p∶=lookup( id.name); //在符號表中利用id識別符號的符號名查詢,如果不存在該識別符號,則報錯,該變數未定義
if p ≠ nil then
Emit(p′∶=′E.place) //如果存在,則生成中間程式碼,該中間程式碼便是賦值的意義
else error}
(2) E→E1+E2 {E.place∶=newtemp; //定義一個臨時變數,用來儲存兩運算元的和,生成中間程式碼
if lookup(E1.name) != nil and lookup(E2.name) != nil then
Emit(E.place′∶=′E1.place′+′E2.place)}
else
error}
(3) E→E1*E2 {E.placeE∶=newtemp;//同上
Emit(E.place′∶=′E1.place′*′E2.place)}
(4) E→-E1 {E.placeE∶=newtemp; //同上
Emit(E.place′∶=′′uminus′E1.place )}
(5) E→(E1) {E.place∶=E1.place}
(6) E→id {p:=lookup(id.name)}; //賦值語句
if p ≠ nil then
E.place=p.place
else error}
2. 型別轉換的語義處理
//對 E→E1*E2 進行型別轉換,比如如果int和float相乘,應該先將int轉換為float
E.place∶=newtemp;
if E1.type=int AND E2.type=int then
begin emit(E.place,′∶=′,E1.place,′*i′, E2.place);
E.type∶=int
end
else if E1.type=real AND E2.type=real then
begin emit (E.place,′∶=′,E1.place,′*r′,E2.place);
E.type∶=real
end
else if E1.type=int and E2.type=real then
begin t∶=newtemp;
emit(t,′∶=′,′itr′,E1.place); //先將E1通過itr操作轉換為float浮點數,利用臨時變數t儲存
emit(E.place,′∶=′,t,′*r′,E2.place);
E.type∶=real
end
else /*E1·type=real and E2.type=int*/
begin t∶=newtemp;
emit(t,′∶=′;′itr′,E2.place);
emit(E.place,′∶=′,E1.place,′*r′,t);
E.type∶=real
end;
}
3. 用數值表示bool值的語義翻譯
//有些語言中,0/1和true/false是可以混用的,有沒有奇怪為啥?其實這編譯語義分析階段的轉換工作
E→E1 or E2
{E.place∶=newtemp;
emit(E.place ′∶=′ E1.place ′or′ E2.place)}
E→E1 and E2
{E.place∶ =newtemp;
emit(E.place ′∶=′ E1.place ′and′ E2.place)}
E→not E1
{E.place∶ =newtemp:;
emit(E.place ′∶=′ ′not′ E1.place)}
E→(E1)
{E.place∶=E1.place}
E→id1 relop id2 //relop是指 < = >三操作符的任意一個
{E.place∶=newtemp;
emit(′if′id1.place relop id2.place ′goto′ nextstat+3);
emit(E.place′∶=′′0′);
emit(′goto′nextstat+2);
emit(E.place′∶=′′1′)}
E→true
{E.place∶=newtemp;
emit(E.place′∶=′ ′1′)} //可以看到在語義翻譯階段,將true翻譯成了1
E→false
{E.place∶=newtemp;
emit(E.place ′∶=′ ′0′)} //將false翻譯成了0