Gcc 1.31 考古(二) 簡單聲明的解析


"走近gcc" 一文下載地址:   http://bbs.chinaunix.net/thread-4096950-1-1.html

我摘錄了一小段以下: html

通常聲明 (學習走近 gcc)
int a; int a[10]; int *a; node

本文遵照簡化原則. gcc 很大, 若是面面俱到的論述, 就會分散您的注意力, 因此只挑
  那些有用的.
文中有時會出現生硬的言辭, 如 "在咱們關心的範圍內宏 AAA 爲空". 按理說這話不應
  出現, 由於技術應該嚴謹, 但若是嚴謹的話.... 結果是大篇幅論述枝節. 因此本着簡化
  原則, 簡單而生硬的告終.
 
因爲咱們如今的知識還不夠, 因此咱們先補補基礎知識.
  基礎知識(1) --- tree
  基礎知識(2) --- gcc 的詞法分析

 

因此這些基礎知識能夠到上文中去學習, 我就能夠少寫一些了. 固然該文所針對的是 gcc 4.5,
不過仍然有些概念在考古 gcc 1.31 的時候有用... 數組

 

=== 而後是咱們從語法文件開始了: 一個最簡單的聲明概覽.

在 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: --
   initdecls -> initdcl($1)
 
沒有關聯的語法動做. 缺省執行 $$=$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 所生成的非終結符的語法值
就能夠了.
相關文章
相關標籤/搜索