作為編譯器後端的第一站,我們首先來實現語義分析器。
正如上一章所說,語義分析器主要用於對抽象語法樹進行語義層面的進一步檢查,並生成符號表。我們也為符號表給出了乙個「記錄任何你想額外記錄下的東西的表」這樣的說了等於沒說的定義。那麼,cmm編譯器的語義分析器到底需要做什麼?其符號表又需要儲存什麼呢?
事實上,出於簡單考慮,cmm編譯器的語義分析器並不做任何的語義檢查,只負責生成符號表。為了明確符號表到底需要記錄什麼,我們需要回顧cmm語言的這幾點功能:
支援賦值(這也包括了支援變數)
支援函式
支援陣列
區分全域性作用域與區域性作用域
也就是說,我們需要通過某種資料結構,將變數名、函式名、陣列大小、作用域資訊等內容全都放進去。似乎很複雜?請接著往下看。
不難發現,變數名和函式名,其實都是抽象語法樹中的某個記號字串;而陣列大小其實也是抽象語法樹中的某個記號字串,只不過我們這裡需要將其轉為整型;那麼,作用域資訊呢?很簡單,我們只需要回答這樣乙個問題:誰產生了作用域?答案當然是函式。也就是說,對於每個函式,其都有乙個作用域,此外,還有乙個全域性作用域。我們還能發現:所有的變數名都位於某個作用域中;而陣列大小是陣列變數的乙個「附加屬性」。
我們需要記錄乙個全域性作用域,和若干函式作用域。且函式作用域由各個函式產生
在每個作用域中,我們需要分別記錄若干變數名
每個變數可能具有陣列大小這一「附加屬性」
有了這幾點思考後,讓我們繼續思考:
作用域該怎麼記錄?用函式名即可。而全域性作用網域名稱只需要使用乙個「非法函式名」即可,我們不妨使用「global」
變數名該怎麼記錄?同理,用變數名即可;此外,我們還需要為每個變數名從0開始按順序編號。至於編號的作用,我們將在**生成器的相關章節中找到答案
陣列大小該怎麼記錄?顯然,用乙個整型即可
作用域是最大的符號表層次,每個作用域中都包含著若干變數名,而每個變數名都具有變數的編號和陣列大小這兩個屬性;如果這個變數不是陣列,那麼陣列大小設為0即可
乙個資料結構在我們的腦海中漸漸浮現:雜湊表。
沒錯,我們可以用雜湊表來同時存放變數名、函式名、陣列大小、作用域資訊等內容。這樣的雜湊表是雙層的:第一層是作用域,我們可以通過函式名或「global」作為鍵進行訪問;第二層是變數名,顯然,我們可以通過某個變數名作為鍵進行訪問;而雜湊值是一對整型,第乙個整型代表了變數的編號,而第二個整型代表了陣列大小。
語義分析器的實現如下所示:
unordered_map>> semanticanalyzer(ast *root)
}}; int globalidx = 0;
/*token_type::decl_list
||---- __decl
||---- [__decl]..
.*/
for (auto declnodeptr: root->sublist);}
}/*
token_type::compound_stmt
||---- __localdecl
||---- __stmtlist
token_type::local_decl
||---- [__vardecl]..
.*/
for (auto vardeclptr: declnodeptr->sublist[3]->sublist[0]->sublist)
;varidx += varsize + 1;}}
else
;globalidx += varsize + 1;}}
return symboltable;
}
語義分析器函式以抽象語法樹的樹根作為輸入,並在整個抽象語法樹中獲知我們所需要的資訊。在進入抽象語法樹之前,我們首先建立了乙個空的符號表,由於全域性作用域一定存在,故我們立即為符號表新增上代表著全域性作用域的鍵;我們還定義了乙個整型globalidx,以跟蹤各個全域性變數的編號。
語法樹的樹根具有一系列的子節點,這些子節點通過decl語法構造而來;而decl又可以推導出「vardecl | funcdecl」,也就是說,整個語法樹上按順序排列著變數的宣告,或函式的宣告
對於函式的宣告,即funcdecl語法,我們可以發現:函式名應該出現在func_decl節點的第二子節點上;而變數名應該出現在兩處:第一處是func_decl節點的第三子節點,即param_list節點的各個子節點上(僅當func_decl節點的第三子節點不是void),這些子節點代表了函式的形參;而第二處則位於func_decl節點的第四子節點,即compound_stmt節點的第一子節點,又即local_decl節點的各個子節點上,這些子節點代表了函式的區域性變數。所有這些子節點的變數名,都位於各個子節點的第二子節點上;而陣列長度,對於形參子節點而言沒有,對於區域性變數子節點而言,則位於各個子節點的第三子節點上(這段說明真的是太繞了)
我們還需要關注的一點是:變數該如何編號?通常情況下,變數從0開始編號,每次加1就行了。但由於cmm中的變數可能是乙個陣列,故在語義分析器的實現中,我們不僅在每次遇到乙個新的變數名後將變數的編號加1,我們還在每次遇到陣列時將編號額外的加上了陣列的長度。這樣做的意義是什麼?我們將在**生成器的相關章節看到答案。
至此,語義分析器就已經實現完成了。通過語義分析器,我們得到了對於**生成器而言非常重要的乙個「資訊中心」——符號表。接下來,就讓我們來看看低階指令的執行器——虛擬機器是怎麼實現的吧。請看下一章:《實現虛擬機器》。
第八章 觸發器
觸發器的基本概述 在sqlserver2005中,儲存過程和觸發器都是sql語句和流程控制語句的集合,就本質而言,觸發器也是一種儲存過程,它是一種在基本表被修改時自動執行的內嵌過程,主要通過事件進行觸發而被執行,而儲存過程可以通過儲存過程名字而被直接呼叫,當對某一張表進行如update,insert...
Linux核心設計與實現 第八章
在第七章中我們討論了中斷處理程式的上半部,在作業系統響應中斷時,存在著一些限制,比如中斷處理可能打斷其他一些重要 或者執行中斷時需要遮蔽其他中斷。這些限制決定上半部的執行過程需要越快越好。下半部環境 第七章中我們了解到實現中斷處理程式的辦法只有一種,但是在本章中我們可以發現下半部有許多實現方法。實現...
總結 CLR Via C (第八章) 構造器
1 類只能擁有類自己的例項構造器 例項構造器不能被繼承 2 以下修飾符不能用於例項構造器 virtual new override sealed abstract 如果類的修飾符為abstract,那麼預設構造器可訪問性為protected,否則為public 3 如果基類沒有提供無參構造器,那麼類...