=== 而后是咱们从语法文件开始了: 一个最简单的声明概览.
在 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 所生成的非终结符的语法值
就能够了.