Ruby 2.x 源代码学习:语法分析 & 中间代码生成 之 方法解析

wisdom0 2019-06-21

前言

本文分析 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 是否在块作用域或局部作用域内做相应的处理

生成 AST

生成 YARV 虚拟机指令

相关推荐