wisdom0 2019-06-21
本文分析 Ruby 如何解析顶层方法定义,假定读者具备《编译原理》基础知识,了解 yacc,bison(自动语法分析器)工具的基本使用
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 语法定义
// parse.y f_arglist : '(' f_args rparen | f_args term ;
Ruby 函数定义可以省略掉左右括号
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 是否在块作用域或局部作用域内做相应的处理