=== 而後是咱們從語法文件開始了: 一個最簡單的聲明概覽.
在 C 語言手冊, 第4章給出的 C 語言聲明的形式:
C 聲明:
declaration: declaration-specifier declarator
聲明: 聲明說明符 聲明器.
例子 "int a", "int" 是聲明描述符, "a" 是聲明器.
specifier: 說明符, declartion-specifier: 聲明說明符, 有時我也稱之爲聲明描述符.
declarator: 聲明器, 給出要聲明的對象和另外一部分類型信息.
例子: "int *a", 聲明說明符是 "int", 聲明器是 "*a".
例子: "int a[10]", 聲明說明符是 "int", 聲明器是 "a[10]".
例子: "int *a, b", 聲明說明符是 "int", 聲明器是 "*a" 和 "b". 注意不是 "*b"
(因爲聲明十分重要, 因此不得很少用一些例子學習了)
咱們的語法學習方法, 從一個例子開始, 列出其解析時將走過的產生式, 而後詳細解釋
每一個非終結符及其產生式的含義.
使用的方法:
1. 自頂向下列出產生式(僅包括與此例子數據相關的產生式), 編號以方便指代.
2. 自底向上地進行歸約, 在過程當中計算每一個左側的非終結符的語法值, 通常該值是右側
的關聯代碼塊計算出來的. 若是未給出該代碼塊, 則缺省爲 $$=$1.
3. 能夠在產生式添加一些調試打印的語句, 如 print("$1 is: ", $1, ", $2 is: ", $2) 等.
格式: 左邊非終結符 -> 右邊符號(終結符, 或非終結符)列表, 圓括號() 中是該符號的語法
值的編號. 花括號 {} 表示該位置有關聯代碼, {$5} 表示該代碼塊的語法值編號, 也用於
指代該代碼塊. 對於有多個產生式的非終結符, 通常選擇簡單的一個.
1. 數據聲明/定義: (選擇簡單的一個)
datadef -> typed_declspecs($1) setspecs($2) initdecls($3) ';'
2. 產生式 1.$1 (選擇一個)
typed_declspecs ->typespec($1) reserved_declspecs($2) {$3}
3. 產生式 2.$1
typespec -> TYPESPEC
4. 產生式 2.$2 (選擇最簡單的空的那個)
reserved_declspecs -> /*empty*/ {$1}
5. 產生式 1.$2
setspecs -> /*empty*/ {$1}
6. 產生式 1.$3 (選擇最簡的)
initdecls -> initdcl($1) {$2}
7. 產生式 6.$1 (選擇簡單的)
initdcl -> declarator($1) maybeasm($2) {$3}
maybeasm -> /*empty*/ 略.
8. 產生式 7.$1 (按例子選擇一個)
declarator ->notype_declarator($1) {$2}
9. 產生式 8.$1 (選擇最簡單的)
notype_declarator -> IDENTIFIER($1)
====
可思考/回答的問題:
1. 爲何有這麼多產生式? -- 由於 C 語法較複雜.
2. 爲何指定非終結符選擇某個變體, 而不是別的變體? -- 按照例子選擇的.
3. 有沒有什麼好辦法研究產生式?
====
下面自底向上地一步一步歸約.
例子: "int a ;"
這個例子有三個 token, 分別是 "int", "a", ';'. 它們從 lexer(詞法器)解析中返回的分別是
"int" -- 返回 TYPESPEC, "a" -- 返回 IDENTIFIER, ';' -- 分號(該字符值就是其詞法值).
當讀入 "int", "a" 時, (可設置調試標誌 yydebug=true 狀態, 查看歸約的具體步驟).
-- 首先歸約產生式 3: --
typespec -> TYPESPEC($1)
"int" 是關鍵字, 詞法器返回爲 TYPESPEC, 則按照此產生式歸約, 此產生式沒有關聯代碼塊,
於是按照缺省, 執行爲 $$=$1.
其中 $1 的值按照 "c-parse.y" 文件中的聲明 (大約在 line 132):
%type <ttype> TYPESPEC ...
表示終結符 TYPESPEC 的類型是 ttype, 在 %union yylval {} 中 ttype 被定義爲 tree ttype.
TODO: 解釋 %union, <ttype>
終結符 TYPESPEC 的語法值是來自於詞法器, "int" 關鍵字對應的是數組 ridpointers[RID_INT]
的值, 這個值是在初始化階段構造的一個 tree 節點(tree node). 這個節點值是一個 tree_code 爲 TYPE_DECL
的樹節點, 表示這是一個類型聲明, 具體這個例子而言, 就是 "int" 類型. 咱們將它
寫做 tree:(type_decl int) 甚至就是關鍵字 int 以方便指代.
TODO: 解釋 ridpointers[], RID_INT
TODO: 解釋 初始化期間創建的 ridpointers[] "int" tree 節點的值及其含義.
TODO: 解釋 tree_code, tree_node, type_decl.
產生式執行缺省動做, $$=$1, 於是非終結符 typespec 在歸約以後, 值爲 tree:(type_decl int),
(這個節點至關於 C 語義 typedef int int)
-- 歸約產生式 4: --
reserved_declspecs -> /*empty*/ {$1}
此產生式右部爲空, 關聯動做爲:
$$ = null.
這樣, 非終結符 reserved_declspecs 的值爲 null.
-- 歸約產生式 2: --
typed_declspecs ->typespec($1) reserved_declspecs($2) {$3}
在歸約產生式 3 中, 咱們知道非終結符 typespec 即在此產生式中 $1 的值爲 tree:(type_decl int),
而 reserved_declspecs $2 的值爲 null, 關聯代碼塊 $3 爲代碼爲:
$$ = tree_cons(null, $1, $2)
值是一個 tree_node, 實際的類型是 tree_list, 簡寫爲 ((type_decl int)), 繼續簡化寫爲 (int).
函數 tree_cons(), 從名字看, 和 Lisp 的 cons 函數類似, 用於構造一個 list. 詳細的之後有機會敘述.
-- 歸約產生式 5 --
setspecs -> /*empty*/ {$1}
產生這個歸約的緣由是 setspecs 出如今歸約非終結符 initdecls 以前, setspecs 不消耗任何
終結符或非終結符(爲空), 可是執行一個語義動做 $1:
current_declspecs = null;
這裏設置了全局變量 current_declspecs 爲 null, 該值在後面歸約產生式7 的時候使用.
而後是對終結符 "a" 的歸約, "a" 在詞法器中被返回爲終結符 IDENTIFIER, 語法值爲 tree_node,
加強的類型爲 tree_identifier.
問題: 解釋 tree_node, tree_list, tree_identifier. (後面解釋)
-- 歸約產生式 9: --
notype_declarator -> IDENTIFIER($1)
IDENTIFIER 標識符的值爲 tree_identifier (一種 tree_node), 咱們將其簡寫爲 (identifier_node x), 若是
不會混淆的話, 可進一步簡寫爲 x.
根據缺省語法動做 $$=$1, 則非終結符 notype_declarator 的值爲 (identifier_node x).
-- 歸約產生式 8: --
declarator ->notype_declarator($1)
根據缺省語法動做 $$=$1, 非終結符 declarator 的值爲 (identifier_node x).
-- 歸約產生式 7: --
initdcl -> declarator($1) maybeasm($2) {$3}
根據歸約產生式8, 知道 declarator $1 的值爲 (identifier_node x).
這裏非終結符 maybeasm 處理 gcc asm 擴展語法, 咱們暫時忽略, 而且認爲 maybeasm 的語法值
即 $2=null.
關聯語法動做 $3 僞代碼爲:
1. decl = start_decl($1, current_declspecs, null)
2. finish_decl(decl, null, $2=null)
這裏執行了連續兩個重要的函數: start_decl(), finish_decl(), 這兩個函數的組合實現對
標識符 (identifier_node x) 的聲明, 聲明被建立爲 tree_decl(tree_node 的一種) 並返回到變量
decl 中. 這個 decl 能夠寫做 (var_decl (identifier_node x) (type_decl int)), 進一步可簡化寫爲
(var_decl int x), 再進一步不混淆的話簡化寫爲 int x, int 指出變量 x 的類型.
這樣, 非終結符 initdcl 的值就是 tree_node:(var_decl x int). 可簡寫爲 int x, 以及 x.
在 start_decl(), finish_decl() 函數中, 創建的變量聲明 (var_decl) 被存儲在當前詞法域中, 在
後續訪問這個標識符("x")時就能查找到它綁定(binding)到一個變量的聲明: var_decl.
-- 歸約產生式 6: --
沒有關聯的語法動做. 缺省執行 $$=$1. (在這個例子中此語法值未使用)
如今非終結符 typed_declspecs, setspecs, initdecls 都已經歸約了, 當遇到 ';' 終結符的時候:
-- 歸約產生式 1: --
1. datadef -> typed_declspecs($1) setspecs($2) initdecls($3) ';'
這樣就達到了本次例子 "int a;" 的結束: 歸約爲一個數據定義 datadef. 其語法值沒有使用.
實際在歸約產生式7 的時候, 已經完成了變量的聲明的語義部分的工做了.
====
基本知識:
tree_node: 樹節點. 在 parse 階段建構的抽象語法樹(abstract syntax tree, AST)的節點類型.
方法:
1. 使用面向對象的概念描述 tree_node 的結構, 層次,繼承關係.
2. 適當抽象, 忽略一些實現細節, 用僞代碼訪問節點的槽(slot).
3. 使用/發明一些書寫方法, 可以用相似 lisp 的方式書寫 tree_node 類型,值,
這樣能夠簡化問題. 畫圖太麻煩, 沒時間畫不少圖.
每一個 tree_node 擁有一個 code (節點代碼), 類型爲 enum tree_code, 根據此 code
肯定此節點的類型.
聲明有一個聯合 tree_node, 指向該聯合的指針類型爲 tree.
typedef union tree_node *tree;
struct tree_common { // 結構(類) tree_node
int uid; // 惟一標識, 自動生成, 不能改變.
tree chain; // 引用到另外一個 tree_node, 通常用於構成鏈.
tree type; // 通常是該節點值/聲明的類型對象.
enum tree_code code; // 樹節點編碼(種類).
bit_flags xxx_attr; // 一組位標誌. 之後詳細說明.
}
以 OO 觀念看, 結構 tree_common 做爲全部其它樹節點結構的基類.
在 gcc 的實現中, 使用一組宏來訪問任意種類的樹節點的 tree_common 結構中的槽.
槽(slot) 指結構 (如 tree_common) 中的字段. 使用槽 (slot) 這個詞, 也是由於在 gcc
代碼的註釋中, 也稱這些字段爲 slot, 同 lisp 語言中稱對象的字段的名稱一致.
聯合 union tree_node 的聲明以下:
union tree_node {
tree_common common;
tree_identifier identifier; // 後面立刻談到.
其它各種 tree_node 在此 union 中狀況相似...
}
先舉一個這種宏的例子, 稍後會遇到更多:
#define TREE_UID(node) ((node)->common.uid)
這裏 node 是 union tree_node 的指針, 所以 (node)->common.uid 就能訪問到 uid 字段.
標識符樹節點結構, 該結構 OO 上看, 從 tree_common 結構派生.
struct tree_identifier extends tree_common {
string pointer; // 此標識符的字符串, 如 "abc"
int length; // 此標識符字符串的長度. 略.
tree global_value, local_value, label_value, implicit_value; // 略, 之後遇到再敘述.
}
或者另外一種寫法:
struct tree_identifier {
tree_common common;
string pointer; // 此標識符的字符串.
// 其它字段略.
}
假設(由詞法器)讀入一個標識符 "foobar", 則在編譯器的符號表(symtab)中插入一個新的
tree_identifier 的對象引用. 該對象的槽 pointer 指向字符串 "foobar", length 爲標識符
長度, code 爲枚舉 enum tree_code 的值 IDENTIFIER_NODE(意爲標識符節點).
咱們借鑑/使用一個文本記法: (identifier_node foobar) 來表示這種節點. 若是不混淆的
狀況下, 咱們直接用 foobar 這個名字表示便可.
訪問 tree_identifier 中的字段, 也是經過幾個宏進行的, 如:
#define IDENTIFIER_POINTER(node) ((node)->identifier.pointer)
爲了方便表述語義, 咱們不使用這麼複雜的宏來訪問節點的槽, 而是使用僞代碼, 例如:
node.uid -- 表示訪問 node 的 uid 這個槽(字段).
node.identifier.pointer -- 表示訪問 tree_identifier 類型的 node 的 pointer 這個槽.
若是沒有混淆, 就用更簡單的寫法: node.pointer 或 node.iden 來表示了. (總之是簡化再簡化)
同理, 用 node.code, node.type, node.chain 等... 訪問其它槽.
結構 struct tree_int_cst extends tree_common 表示整數常量. 但咱們忽略細節, 直接使用
node.int_const 來表示這個常量整數的值, 而不考慮 gcc 實現中實際的方式. (它使用兩個
int32 的數 來軟件實現 int64 類型的整數, 咱們如今暫略去此一實現細節)
一個 tree_int_cst 的節點咱們使用文本記法: (int_const 123), 123 表示此常量的值, 若是
沒有歧義的狀況下, 通常直接簡寫爲 123.
結構 struct tree_list extends tree_common {
tree purpose; // (可選)這個節點的值的說明, 用途, 目的等. 不少時候爲 null.
tree value; // 這個節點的值.
}
這個結構很是相似於 Lisp 的 list, list 的 car 對應這個 value 字段, cdr 對應 tree_common
中的 chain 字段. 在 gcc 中, 常用此結構將一組對象構成一個列表. 咱們說它像 lisp
是由於程序中使用此結構的不少函數, 都有相似與 lisp 的對應函數的名字... 合理推測做者必定
是深受 lisp 影響的.
咱們用 (list value next-chain-value ...) 相似於 lisp 中的記法來書寫出一個 list:
例子: 設有一個列表, 裏面有 tree_node 節點 (list (int_const 123) (identifier_node x) (identifier_node y))
等, 則咱們還能夠簡寫爲 (list 123 x y), 或者按照 lisp 更簡單的寫法: '(123 x y)
例如非終結符 typed_declspecs 的語法值就是這種 tree_list 的節點, 對於以下聲明:
"static const unsigned int x;"
則歸約到的非終結符 typed_declspecs 的值是 '(static const unsigned int).
結構 tree_decl 用於表示一個聲明, 能夠是一個變量聲明, 其 code=VAR_DECL; 或一個
類型聲明, 其 code=TYPE_DECL, 或參數, 函數, 等多種聲明. (之後遇到再研究).
struct tree_decl extends tree_common {
string filename; // 所在文件, 估計通常用於調試信息.
int linenum; // 所在文件行.
tree name; // 這個聲明的名字, 指向 tree_identifier 節點.
... 其它不少 slot 暫略.
}
對一個 code=VAR_DECL 的變量聲明節點來講, 最主要的(當前關心的)屬性槽是變量的
名字 -- name, 和變量的類型 -- type. 例如 "int x" 這樣的聲明, 就產生一個
name=(identifier_node x), type=(type_decl int) 這樣的 tree_decl 節點.
咱們將其寫作 (tree_decl type=(type_decl int) name=(identifier_node x)), 按照
咱們天然的簡化傾向, 以及這是一個特定的 var_decl 類型的 tree_decl, 因此把它簡寫做
(var_decl int x), 而後更簡寫作 x.
在 gcc 1.31 的這個版本, 全部聲明都使用一個 tree_decl 結構存放的, 因此有點各類
字段混淆一塊兒. 我看後面版本的 gcc 根據 code 區分了多種 tree_decl 結構, 這樣彷佛有專門
的 var_decl 結構了, 就不至於混淆字段了.
對幾種 tree_node 先簡單說明一點, 以大體能理解 parse 所生成的非終結符的語法值
就能夠了.