前言
本文分析 Ruby 如何解析顶层方法定义,假定读者具备《编译原理》基础知识,了解 yacc,bison(自动语法分析器)工具的基本使用
BNF 语法
parser.y 包含了 Ruby 语言所有的语法,下面是和函数相关的片段(parser.y 文件有 1 W 多行)
我们将注意力集中在 函数定义的语法上,先忽略掉 YACC 语法动作(下同)
// parse.y
primary : k_def fname f_arglist bodystmt k_end
k_def,关键字 def
fname,函数名称
f_arglist,函数参数列表
bodystmt,函数内部语句块
k_end,关键字 end
f_arglist
从名字可以看出 f_arglist 表示函数参数列表,下面是 f_arglist 语法定义
// parse.y
f_arglist : '(' f_args rparen
| f_args term
;
Ruby 函数定义可以省略掉左右括号
f_args
Ruby 支持各种 "奇葩" 的函数参数传递方式,f_args 的语法定义考虑了各种组合情况,先从最简单的开始:
// parse.y
f_args : f_arg opt_args_tail
f_arg : f_arg_item | f_arg ',' f_arg_item
f_arg_item : f_arg_asgn | tLPAREN f_margs rparen
f_arg_asgn : f_norm_arg
f_norm_arg : f_bad_arg | tIDENTIFIER
每个函数参数使用 逗号 分割,如果不考虑 (x) 这种类型的参数,每个参数都是一个 tIDENTIFIER(标识符)
作用域
语法分析的上下文
语法分析是个极其复杂,繁琐的过程,Ruby 使用 parser_params 结构体作为语法分析上下文(context)的抽象,它保存了语法分析(包括词法)过程中的状态变量,下面仅列出和作用域相关的字段
// parse.y or parse.c
struct parser_params {
...
struct local_vars *lvtbl;
...
}
struct local_vars {
struct vtable *args;
struct vtable *vars;
struct vtable *used;
struct local_vars *prev;
stack_type cmdargs;
}
struct vtable {
ID *tbl;
int pos;
int capa;
struct vtable *prev;
};
local_vars 结构体保存了参数和本地变量,并通过 prev 指针指向上一级 local_vars(栈)
作用域链(栈)
现在可以来看看函数定义的 YACC 语法动作
// parse.y
k_def fname
{
local_push(0);
$<id>$ = current_arg;
current_arg = 0;
}
{
$<num>$ = in_def;
in_def = 1;
}
f_arglist
bodystmt
k_end
local_push 会新建一个 作用域,并连接到作用域栈中
// parse.y or parse.c
static void local_push_gen(struct parser_params*,int);
#define local_push(top) local_push_gen(parser,(top))
#define lvtbl (parser->lvtbl)
static void
local_push_gen(struct parser_params *parser, int inherit_dvars)
{
struct local_vars *local;
// 分配内存
local = ALLOC(struct local_vars);
// 将 local 链接到作用域链
local->prev = lvtbl;
// 分配内存
local->args = vtable_alloc(0);
local->vars = vtable_alloc(inherit_dvars ? DVARS_INHERIT : DVARS_TOPSCOPE);
local->used = !(inherit_dvars &&
(ifndef_ripper(compile_for_eval || e_option_supplied(parser))+0)) &&
RTEST(ruby_verbose) ? vtable_alloc(0) : 0;
# if WARN_PAST_SCOPE
local->past = 0;
# endif
local->cmdargs = cmdarg_stack;
CMDARG_SET(0);
// 更新当前作用域,注意:lvtbl 是一个宏定义!!!
lvtbl = local;
}
参数
我们已经知道在定义一个函数的时候,语法分析程序会新建一个 local_vars 并添加到作用于链中,那函数参数是如何添加到作用域中的呢?我们来看一下 函数参数的一条语法规则:
// parse.y
f_arg_asgn : f_norm_arg
{
ID id = get_id($1);
arg_var(id);
current_arg = id;
$$ = $1;
}
;
答案就在 arg_var 方法里头:
// parse.y or parse.c
static void arg_var_gen(struct parser_params*, ID);
#define arg_var(id) arg_var_gen(parser, (id))
static void arg_var_gen(struct parser_params *parser, ID id)
{
vtable_add(lvtbl->args, id);
}
static void vtable_add(struct vtable *tbl, ID id)
{
if (!POINTER_P(tbl)) {
rb_bug("vtable_add: vtable is not allocated (%p)", (void *)tbl);
}
if (VTBL_DEBUG) printf("vtable_add: %p, %"PRIsVALUE"\n", (void *)tbl, rb_id2str(id));
// tbl 空间不够,扩容~
if (tbl->pos == tbl->capa) {
tbl->capa = tbl->capa * 2;
REALLOC_N(tbl->tbl, ID, tbl->capa);
}
将 id 放入 tbl
tbl->tbl[tbl->pos++] = id;
}
局部变量
上文介绍了函数参数如何加入到作用域中,那局部变量呢?局部变量是不是也有类似 arg_var 方法调用呢?我们先想一下通常情况下什么时候会创建一个局部变量:对于 Ruby 这类动态脚本语言,没有像C语言中的变量声明语法,所以在变量赋值(首次使用)的时候就会自动创建。我们来验证一下这个猜想,还是先来看一段语法规则:
// parse.y
lhs : user_variable
{
$$ = assignable($1, 0);
/*%%%*/
if (!$$) $$ = NEW_BEGIN(0);
}
assignable 函数比较复杂,下面仅列出和局部变量定义相关的代码段:
// parse.y or parse.c
static NODE* assignable_gen(struct parser_params *parser, ID id, NODE *val) {
switch (id_type(id)) {
case ID_LOCAL:
if (dyna_in_block()) {
if (dvar_curr(id)) {
...
} else if (dvar_defined(id)) {
...
} else if (local_id(id)) {
...
} else {
dyna_var(id)
}
} else {
if (!local_id(id)) {
local_var(id);
}
}
}
}
根据 id 是否在块作用域或局部作用域内做相应的处理
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。