http://hi.baidu.com/xosjfkixdgbqvyr/item/16367dfaf8e56b0984d278f0
最近结合语法树的生成及解释执行的过程分析了一下C-Ruby中RNode结构体的设计。
RNode是用于记录C-Ruby解析源文件过程中所生成的语法树结点的重要数据结构。
在C-Ruby解析源文件的过程中,会在读入源文件内容的同时建立相应的语法结点,
即RNode变量,并以链表的形式建立起一个语法树,维护语法结点的依赖关系。
在C-Ruby要对源文件进行解释执行的时候,就会从这个语法树链表的首结点开始,逐个
遍历各个语法结点并解释执行。
RNode 的数据结构的定义如下: // 位于 node.h中
typedef struct RNode {
unsigned long flags; //标志位,用于标识结点类型,具体结点类型也定义
//在node.h中,从C-Ruby的源码中来看,语法结点所对应
//的源码行号也是存放在的flags中的
char *nd_file; // 结点所对应的Ruby源文件名
// 在RNode结构体中,定义了三个Union成员,比较有趣。
// 之所以使用Union是希望减少语法树所占用的内存开销
//因为C-Ruby是一个解释器,其解释的对象就是RNode构成的
//语法树,为了降低语法树的内存开销,松本在设计RNode的时候采用
//了三个Union变量。针对不同类型的语法结点,u1,u2,u3会使用不同
//的Union成员,而且,即便使用了同一个成员,也会有不同的含义,
//在node.h中专门定义了一组宏来简化对RNode的操作:
// 如nd_head, nd_next, nd_cond,还有我们下面会提到的
//nd_mid, nd_args 等等
// 在u1, u2, u3的定义中,VALUE成员提供了对u1, u2, u3执行写操作的
//统一接口,而其他的Union成员定义则提供了可能被不同语法结点
//使用的多种读接口,
//比如函数调用结点会使用u2的id 成员,
//而对于另外一种结点,则可能会使用u2的argc成员。
//但是向u2中写入内容,则都是通过VALUE成员写入的.
//我理解这样设计的目的是既希望通过Union来节约内存,还希望能够简化
//语法结点的创建工作。
union {
struct RNode *node;
ID id;
VALUE value;
VALUE (*cfunc)(ANYARGS);
ID *tbl;
} u1;
union {
struct RNode *node;
ID id;
long argc;
VALUE value;
} u2;
union {
struct RNode *node;
ID id;
long state;
struct global_entry *entry;
long cnt;
VALUE value;
} u3;
} NODE;
对RNode的基本结构有了一定了解之后,不妨以一个简单的例子来分析RNode,我采用的
例子文件test.rb仅包含一行内容:
print "hello";
对于test.rb,C-Ruby会先建立起一个由RNode结点构成的语法树,然后再对其进行解释执
行。
语法树的生成流程大致可以描述如下:
词法模块对源码流输入print "hello";进行分解,返回语法模块可以识别的终结符,
语法模块则根据parse.y中编写的语法规则执行相应的移入,归约动作,在执行归约动
作的时候调用相应语法规则的语义动作,从而完成语法树生成的工作。
从源码输入规约到顶层语法描述的完整过程序列有些繁杂,中间夹杂一些中转规则
和状态,为了简化描述,我对整个语法归约过程进行了一定的简化:
源码输入: print "hello";
==> operation "hello"; ( operation: print )
注:括号里表示为了得到本条语法序列,执行语法归约动作所使用的相应语法规则)
==> operation strings; (strings: "hello" )
==> operation primary; (primary: strings)
==> operation arg; (arg: primary)
==> operation arg_value;( arg_value: arg)
==> operation args; ( args: arg_value )
==> operation call_args; ( call_args: args )
==> operation open_args; ( open_args: call_args )
==> operation command_args; ( command_args: open_args )
==> command; (command: operation command_args )
==> command_call; ( command_call: command )
==> expr; ( expr : command_call )
==> stmt; ( stmt : expr )
==> stmts; ( stmts: stmt )
==> stmts opt_terms ( opt_terms: ';' )
==> compstmt ( compstmt: stmts opt_terms )
==> program (program: compstmt)
从源码print "hello"; 开始逐步归约,每执行一个相应动作,就会创建一个相应
的RNode结点(也有一些归约动作并不会创建RNode结点,而只是传递已经创
建好的RNode结点指针),及至归约到program语法符号之时,整个语法树就已经建
立起来了。语法树的根结点存放在一个全局的RNode指针变量, ruby_eval_tree,
定义在parse.y中。
在test.rb中仅包含一个函数调用,所以在语法树生成过程中会调用new_fcall()(定义于
parse.y中)创建一个NODE_FCALL类型的结点。
static NODE*
new_fcall(m,a)
ID m; // 函数ID,具体到test.rb即是print函数的相应ID
NODE *a; // 函数参数列表对应的语法结点,具体到test.rb,对应于
// "hello"
{
return NEW_FCALL(m,a);
}
可以看到new_fcall()又调用了NEW_FCALL来完成实际的生成函数调用结点的动作,
NEW_FCALL则是一个定义在node.h的宏。
#define NEW_NODE(t,a0,a1,a2) rb_node_newnode((t),(VALUE)(a0),(VALUE)(a1),(VALUE)(a2))
#define NEW_FCALL(m,a) NEW_NODE(NODE_FCALL,0,m,a)
NODE*
rb_node_newnode(type, a0, a1, a2) // 定义在parse.y
enum node_type type;
VALUE a0, a1, a2;
{
NODE *n = (NODE*)rb_newobj();
n->flags |= T_NODE;
nd_set_type(n, type);
nd_set_line(n, ruby_sourceline);
n->nd_file = ruby_sourcefile;
n->u1.value = a0;
n->u2.value = a1;
n->u3.value = a2;
return n;
}
从NEW_FCALL中可以看出,对于函数调用结点,RNode的u1成员是不会使用的,u2成员则
会用来存放标识函数的ID,u3则用来存放函数调用的相关参数列表的信息。在解释
执行的过程中,对于NODE_FCALL类型的结点也会通过访问u2,u3中相应的Union成员来获得
函数调用的相关信息。
最后再来看一下语法树的解释执行过程(后面简称为eval过程):
eval过程始于ruby_run()函数(定义于eval.c),基本的调用图如下所示:
eval_node()方法以ruby_eval_tree作为起始结点调用rb_eval()函数,rb_eval()内部
又会根据结点类型去执行相应的解释动作,并且通过RNode中定义的指针成员获得语法树
中后续需要处理的RNode对象的指针,如此往复,直至完成对整个语法树的解释执行动作。
具体到test.rb这个例子,对于print "hello"所生成的NODE_FCALL的语法结点,eval过程
会执行如下的动作:
case NODE_FCALL: // 定义于eval.c中的rb_eval()函数
{
// node 是一个定义在rb_eval()局部的RNode *
int argc; VALUE *argv; /* used in SETUP_ARGS */
TMP_PROTECT;
BEGIN_CALLARGS;
// nd_args是一个定义在node.h中的宏
//#define nd_args u3.node
SETUP_ARGS(node->nd_args); // 配置参数, 会向argc, argv
//中写入数据
END_CALLARGS;
ruby_current_node = node;
SET_CURRENT_SOURCE();
// nd_mid是一个定义在node.h中的宏
//#define nd_mid u2.id
// 通过nd_mid获得语法结点中存放的函数ID,
// 通过nd_args获得语法结点中存放的参数信息
// 如此一来,就具备解释执行NODE_FCALL结点所需的足够信息了
result = rb_call(CLASS_OF(self),self,node->nd_mid,argc,argv,1,self);
}