Rubyに新しい予約語を追加してみた その2

大学の演習「大規模ソフトウェアを手探る」の成果と過程をまとめます。
その1では、if文のthen~endの代わりにブレース{~}を使えるようにしようという目標を掲げましたが達成できず、その過程で
* 新しく予約語を定義し、既存の予約語と同じ役割を持たせる

ことができるようになり、elsifと同じ役割を持つelseifという予約語を実装しました。

第二目標

私たちは次に、「Rubyには所謂main関数が無い」という点に着目しました。
例えばJavaではmain関数はそのファイルを直接実行した時にしか呼ばれませんので、ライブラリのテストコードを記述する時などに使われます。
Rubyで同様のことを実現しようとした場合、以下のように書くそうです。

#既存の書き方
if __FILE__ == $0
  #test code
end

__FILE__はそれが書かれているファイル名が入る予約語
$0は実行時にコマンドライン引数で指定したファイル名が入る特殊変数です。
別のプログラムからrequireされた場合は両者は一致しませんから、上記のif文の中身は実行されません。

さてこの書き方、"おまじない"と言われることもあり初見では意味が分かりづらいように感じます。
できるならいいじゃん、と思うかもしれませんが、もっと一目瞭然に
例えば下記のように記述できれば楽だし分かりやすいと私たちは考えました。

#目標
if __MAIN__
    #test code
end

つまり、__MAIN__という予約語を新しく定義し、
* 実行時にrequireされたファイルの中ではfalseに
* それ以外ではtrueに

なるような機能を持たせよう、というのが私たちの新しい目標です。
ちなみにifを残した形にしたのは、中身が実行される場合とされない場合があるということが一目で分かるようにするためです。

Rubyが何をしているか

これは私たちが上述の目標に取り組む過程で分かってきたことなのですが、説明の都合上順番を前後させていただきます。

Rubyスクリプトを読み込んでから実行するまでの流れを大まかにまとめたものが下図です。
f:id:suzuhoka:20151105003959p:plain

  1. スクリプトの文字列を1文字ずつ読み、意味のある語句毎にトークンとしてまとめる。
  2. そのトークンの並びを見て文の構造がどうなっているか解析し、木構造(構文木)にする。
  3. 構文木からYARVという仮想マシンのための命令列を生成する。
  4. 続く

という流れになっています。
構文木というのは、例えば__FILE__==$0という部分の構文木
f:id:suzuhoka:20151105004013p:plain

というふうになります。
ruby hoge.rb --dump parsetreeのようにオプションをつけて実行するとそのスクリプト構文木を見ることができます。

また、YARV命令列はソースコードに含まれるtool/parse.rbを使うと見ることができて、
ruby /rubyソースフォルダのパス/tool/parse.rb hoge.rbとすると例えば

# ----------------------------------------------------------------------
# target program:
# ----------------------------------------------------------------------
if __FILE__ == $0
    puts "a"
end
# ----------------------------------------------------------------------
# disasm result:
# ----------------------------------------------------------------------
== disasm: <RubyVM::InstructionSequence:<main>@hoge.rb>=================
0000 trace            1                                               (   1)
0002 putstring        "hoge.rb"
0004 getglobal        $0
0006 opt_eq           <ic:2>
0008 branchunless     23
0010 trace            1                                               (   2)
0012 putself          
0013 putstring        "a"
0015 send             :puts, 1, nil, 8, <ic:1>
0021 leave                                                            (   1)
0022 pop              
0023 putnil                                                           (   2)
0024 leave            
# ----------------------------------------------------------------------

のようになります。

MAIN を実現する3つの方法

さて、私たちは第一目標でparse.y構文解析の部分を探っていたので、最初は構文解析器に処理を追加して__MAIN__を実現しようとしていました。
しかしどうも上手くいかないので、まずは第一歩として最も単純な方法で実装することにします。

1.字句解析時に処理する

そもそも__MAIN__とは、__FILE__==$0の書き換えです。
ですから、スクリプトを1文字ずつ読んでいく字句解析で__MAIN__という文字列を発見したらその時点で __FILE__==$0という文字列に置き換えて処理してしまおう、というのがこの方法です。  

まずは字句解析をしている部分を見つけなければなりませんが、それにはデバッガが役に立つでしょう。
デバッガでRubyの動きを追っていくと、parse.cparser_yylexという関数が何度も呼ばれているようです。
文字列検索するとこれはparse.yの7877行目にあり、その中の7917行目から8535行目にかけてのswitch文で1文字ずつ処理しているようでした。
その終盤である8512行目に以下のような部分を見つけました。  

//parse.y 8512行目から
    case '_':
    if (was_bol() && whole_match_p("__END__", 7, 0)) {
          ruby__end__seen = 1;
          parser->eofp = Qtrue;
        #ifndef RIPPER
          return -1;
        #else
              lex_goto_eol(parser);
              ripper_dispatch_scan_event(parser, k__END__);
              return 0;
        #endif
    }
    newtok();
    break;

__END__というのはRubyスクリプトの終わりを表し、これ以降は無視されます。
__MAIN__の置き換えの処理もここに書いてしまいましょう。

さて、元の文字列がどこに保管されているのかを探します。
__END__の比較に使われていたwhole_match_pという関数名で検索すると、どうやらparser_paramsという構造体が怪しそうです。
これを検索すると

//parse.y 209行目から
/*
    Structure of Lexer Buffer:

 lex_pbeg      tokp         lex_p        lex_pend
    |           |              |            |
    |-----------+--------------+------------|
                |<------------>|
                     token
*/
struct parser_params {
//・・・
    const char *parser_lex_pbeg;
    const char *parser_lex_p;
    const char *parser_lex_pend;
//・・・
};

ご丁寧にコメントで図まで描いてありました。
これをみると、lex_pbegが現在解析中の行の先頭、lex_pendが行の末尾、lex_pが見ている文字の場所、を指すポインタでしょうか。
parser_yylexにはparser_yylex(struct parser_params *parser)として渡されていたので、これを参考に以下のように処理を追加します。

//parse.y 8512行目から 変更後
    case '_':
    /*追加ここから*/
        if (strncmp(parser->parser_lex_p, "_MAIN__", 7) == 0) {
            memmove(parser->parser_lex_p + 11, parser->parser_lex_p + 7, strlen(parser->parser_lex_p + 7) + 1);
            parser->parser_lex_pend = parser->parser_lex_pend + 4;
            const char *src = "_FILE__==$0";
            strncpy(parser->parser_lex_p, src, 11);
            goto retry2;
        }
    /*追加ここまで*/
        if (was_bol() && whole_match_p("__END__", 7, 0)) {
//・・・

parser_lex_pはこの時点では現在見ている文字の次の文字を指していたので上のようになっています。
* __MAIN__より__FILE__==$0の方が4文字分大きいのでmemmove__MAIN__より後ろの文字を4文字ずらします。
* 行の末尾を指していたparser_lex_pendも4文字後ろを指すように貼り替えます。
* strncpy__MAIN____FILE__==$0に置き換えます。
* 置き換え後の文字列を再び字句解析器にかけるため、ラベルretry2まで飛ばします。

ラベルretry2はswitch文の直前に貼りました。
元々retryというラベルがあったのですが、ここに飛ぶと次の文字から字句解析してしまうので新しく設けています。

//parse.y 7915行目から 変更後
//・・・
retry:
  last_state = lex_state;
  c = nextc();
retry2://追加
  switch (c) {
//・・・

さて、これでちゃんと動くのですが、ぶっちゃけこれはルール違反です。
parser_lex_pなどにconstと付いていたように元の文字列を書き換えることは想定されていませんし、 同じようなことをしている箇所も見当たりません。

ではどのような実装が正解なのか、というのを考えていきます。

2.構文解析時に処理する

parse.y構文解析のところを読むと分かりますが、Rubyではfor文はeach文に読み替えられて構文木が作られています。
他にも三項演算子はif文へ、if修飾子はif文へ、など同様に読み替えられている文法はいくつもあり、
「他の文法で代用できる文法は構文解析で読み替える」という設計思想が見て取れます。
今、私たちが目標とする__MAIN____FILE__==$0に読み替えることが可能ですから、 他の例を真似て__MAIN__から生成される構文木__FILE__==$0から生成される構文木と全く同じもの(下図)にすると良いでしょう。
f:id:suzuhoka:20151105004013p:plain

そのためにまず、__FILE__==$0構文解析されて構文木になるまでの過程を調べます。
そこで活躍するのが、-yオプションです。
ruby -y hoge.rbとすると、そのスクリプト構文解析されていく様子が表示されます。
例えば、

Reducing stack by rule 483 (line 4429):
   $1 = token keyword__FILE__ ()
-> $$ = nterm keyword_variable ()

と書いてあれば、parse.yの4429行目のルールが適用されたことになります。

//parse.y 4429行目付近
keyword_variable: keyword_nil {ifndef_ripper($$ = keyword_nil);}
        | keyword_self {ifndef_ripper($$ = keyword_self);}
        | keyword_true {ifndef_ripper($$ = keyword_true);}
        | keyword_false {ifndef_ripper($$ = keyword_false);}
        | keyword__FILE__ {ifndef_ripper($$ = keyword__FILE__);}
        | keyword__LINE__ {ifndef_ripper($$ = keyword__LINE__);}
        | keyword__ENCODING__ {ifndef_ripper($$ = keyword__ENCODING__);}
        ;

これを追っていき、__FILE__==$0がどのように推移して各所でどのような処理が行われたのかをまとめると下のようになりました。

__FILE__:keyword__FILE__→①→keyword_variable→②→var_ref→primary→arg
==      :tEQ
$0      :tGVAR→user_variable→③→var_ref→primary→arg
全体     :arg tEQ arg→④→arg→expr→⑤→expr_value

①ifndef_ripper($$ = keyword__FILE__);
②if (!($$ = gettable($1))) $$ = NEW_BEGIN(0);

③if (!($$ = gettable($1))) $$ = NEW_BEGIN(0);

④$$ = call_bin_op($1, tEQ, $3);
⑤value_expr($1);  $$ = $1;    if (!$$) $$ = NEW_NIL();

ちょっとこれだけだと分からないので具体例を見てみましょう。
例えば④の処理は

//parse.y 2260行目付近
arg :
//・・・
    | arg tEQ arg
        {
        /*%%%*/
        $$ = call_bin_op($1, tEQ, $3);
        /*%
        $$ = dispatch3(binary, $1, ID2SYM(idEq), $3);
        %*/
        }
//・・・

となっています。call_bin_opという関数で木を作り、それを$$に代入することで新しいargというトークンに持たせるイメージです。
$1はコロン:右側に書かれたパターンの一番目のトークンを指します。つまりこの場合はtEQの左に書いてあるargです。
ちなみに/*%%%*/前者/*%後者%*/の2つの処理がありますが、前者がメインの構文解析用の処理で、 後者はripperツールという構文解析の結果を文字列として表示させるツールのための処理です。

$$に代入される関数で木を作っているので、この関数の中身を読んで実際にどのようにして木が作られているのかを調べます。
そして、それらを使って先に示した図のような木を作ればいいのです。
(調べる過程は長いので省略します。)
結果、以下のようにすれば目的の木を作成できるということが分かりました。

NEW_CALL(
        NEW_STR(rb_str_dup(ruby_sourcefile_string)),
        tEQ,
        NEW_ARRAY(NEW_GVAR(rb_intern("$0")))
        );

図と見比べてもらうと構造が分かりやすいと思います。
殆どは実際に使われている関数の中身をそのまま真似るだけで良かったのですが、NEW_GVARの引数がID型というもので、 これを生成するためのrb_internという関数を発見するのに苦労しました。
結局、どうにも行き詰まってparse.yの膨大なソースコードをぼーっと眺めていたら偶然見つけたので、どう解説したらいいものか...

さて、目的の木は作れたので実装していきます。
まずは__MAIN__という新しい予約語を定義します。__FILE__を真似るといいでしょう。

//defs/keywords 変更後
__FILE__, {keyword__FILE__, keyword__FILE__}, EXPR_END
__MAIN__, {keyword__MAIN__, keyword__MAIN__}, EXPR_END  //追加

次にkeyword__MAIN__というトークンを新しく定義します。これもparse.ykeyword__FILE__と検索して真似るだけです。

//parse.y 変更後
//797行目付近
    keyword__FILE__
    keyword__MAIN__ //追加
//1974行目付近
reswords  : keyword__LINE__ | keyword__FILE__ | keyword__ENCODING__
            | keyword__MAIN__   //追加
//10984行目付近
    {keyword__FILE__,   "__FILE__"},
    {keyword__MAIN__,   "__MAIN__"},

次に、keyword__MAIN__用の文法規則を追加する場所を決めます。 基本的にはkeyword__FILE__などと同じ扱いをすればいいはずなので、まず以下のようにします。

//parse.y 4429行目付近 変更後
keyword_variable: keyword_nil {ifndef_ripper($$ = keyword_nil);}
        | keyword_self {ifndef_ripper($$ = keyword_self);}
        | keyword_true {ifndef_ripper($$ = keyword_true);}
        | keyword_false {ifndef_ripper($$ = keyword_false);}
        | keyword__FILE__ {ifndef_ripper($$ = keyword__FILE__);}
        | keyword__MAIN__ {ifndef_ripper($$ = keyword__MAIN__);}    //追加
        | keyword__LINE__ {ifndef_ripper($$ = keyword__LINE__);}
        | keyword__ENCODING__ {ifndef_ripper($$ = keyword__ENCODING__);}
        ;

このkeyword_variableというトークンは、先ほどまとめた②の推移でgettableという関数に入れられるようです。
これを探っていくと8903行目にgettable_genという関数が定義されており、これが本体のようです。
ここで各予約語に対応する木を生成しているようなので処理を書き足します。

//parse.y 8902行目付近 変更後
static NODE*
gettable_gen(struct parser_params *parser, ID id)
{
    switch (id) {
      case keyword_self:
    return NEW_SELF();
      case keyword_nil:
    return NEW_NIL();
      case keyword_true:
    return NEW_TRUE();
      case keyword_false:
    return NEW_FALSE();
      case keyword__FILE__:
    return NEW_STR(rb_str_dup(ruby_sourcefile_string));
      case keyword__LINE__:
    return NEW_LIT(INT2FIX(tokline));
      case keyword__ENCODING__:
    return NEW_LIT(rb_enc_from_encoding(current_enc));
/* 追加ここから */
      case keyword__MAIN__:
    return NEW_CALL(NEW_STR(rb_str_dup(ruby_sourcefile_string)), tEQ, NEW_ARRAY(NEW_GVAR(rb_intern("$0"))));
/* 追加ここまで */
    }
//・・・

変更後、ビルドしてif __MAIN__ then hogehoge endのようなスクリプトを実行したりrequireで呼んだりするとちゃんと思惑通りの動きをしてくれました。
--dump parsetreeオプションをつけて実行してみると実際に__FILE__==$0と全く同じ構文木が出来上がっているのが分かります。

さて、これで一応"ルール違反ではない"実装はできたのですが、これは
* __MAIN____FILE__==$0の省略形である

と定義した場合の実装です。
また別の定義をした場合の実装方法を考えます。

3.YARV命令列へのコンパイル時に処理する

今度は
* __MAIN__は他のファイルから呼ばれた時にfalse、それ以外はtrueとなる予約語である

と定義してみましょう。
つまり、仮にRuby__FILE__$0が無くても動くようにするということです。

__MAIN__は実行中に値が変わることのない定数で、その値はtruefalseです。 したがって、予約語truefalseと同じように扱い、truefalseが実際に役割を果たす段階で、場合に応じてどちらかと同じ役割を果たすようにすればいいでしょう。

さて、truefalseが役割を果たす場所ですが、前述のtool/parse.rbを使うとYARV命令列の時点で既にtruefalseの違いに応じた命令列になっていることが分かります。
つまり__MAIN__YARV命令列へのコンパイル時に処理すればいいということです。その過程を担うのは主にcompile.cです。

次に、trueになるかfalseになるかを決めるには__MAIN__が書かれているファイルと実行時にコマンドライン引数で指定したファイルが同一かどうか調べる必要が有ります。

__MAIN__が書かれているファイル名は__FILE__にあたりますが、__FILE__構文木になった時点で既にファイル名の文字列に置き換わっていました。
f:id:suzuhoka:20151105004013p:plain

この部分のノードはparse.y中で以下のような関数で作られていました。

NEW_STR(rb_str_dup(ruby_sourcefile_string))

このruby_sourcefile_stringにファイル名の文字列が入っているわけですが、これを探ってみると、パーサーが呼び出されるときの引数が元になっているようです。
つまり、パースが終わった後のコンパイル時にruby_sourcefile_stringを見ることができないので、ノードに文字列としてファイル名の情報を持たせていたわけです。

これに習って__MAIN__のノードにもファイル名の情報を持たせることにしましょう。
まず、__MAIN__専用のノードNODE_MAINを定義します。
f:id:suzuhoka:20151105004019p:plain

NODE_TRUEなどでプロジェクト内検索すると、hode.hで定義されているようでした。

//node.h 22行目から 変更後
enum node_type {
//・・・
    NODE_TRUE,
#define NODE_TRUE        NODE_TRUE
    NODE_FALSE,
#define NODE_FALSE       NODE_FALSE
    NODE_MAIN,                      //追加
#define NODE_MAIN       NODE_MAIN   //追加
//・・・
//457行目付近
#define NEW_TRUE() NEW_NODE(NODE_TRUE,0,0,0)
#define NEW_FALSE() NEW_NODE(NODE_FALSE,0,0,0)
#define NEW_MAIN() NEW_NODE(NODE_MAIN,0,0,0)  //追加
//・・・

ついでにNODE_MAINを作るマクロNEW_MAINも定義しました。

次に、構文解析__MAIN__があったらNODE_MAINを作って木に加えるように変更します。
予約語の定義方法は2.と同じなので省略します。

//parse.y 8902行目付近 変更後
        case keyword__MAIN__:
        return NEW_MAIN(rb_str_dup(ruby_sourcefile_string));

これでcompile.c__MAIN__が書かれたファイル名を知ることができるようになりました。

次は、実行時にコマンドライン引数で指定したファイル名を取得する方法を調べます。
これは$0にあたるものなので、少し大変ですが$0でプロジェクト内検索してみます。
すると、ruby.cの中で以下のような箇所を見つけました。

//ruby.c 1919行目
rb_define_hooked_variable("$0", &rb_progname, 0, set_arg0);

どうやらrb_prognameが目的のファイル名のようです。 これは同じくruby.cで以下のように定義されていました。

//ruby.c 1172行目
#define rb_progname      (GET_VM()->progname)

GET-VM()vm_core.hで定義されており、compile.cvm_core.hをインクルードしているので同様の方法で目的のファイル名を取得することができます。

さて、これで準備が整いました。
早速compile.cに追記していきます。

//compile.c 25行目から 変更後
/* 追加ここから */
#define rb_progname      (GET_VM()->progname)

static int is_orig_progname(NODE *node){
  if(nd_type(node) != NODE_MAIN){
    return 0;
  }
  if(strcmp(StringValuePtr(node->nd_lit), StringValuePtr(rb_progname)) == 0){
    return 1;
  }else{
    return 0;
  }
}
/* 追加ここまで */

まずは利便性のため、NODE_MAINを渡してそれが持つファイル名とrb_prognameのファイル名を比較する関数を定義します。
実は、これらのファイル名は通常の文字列ではなくVALUE型というデータになっています。
これは
* C言語は変数に型があり、データに型がない
* Rubyは変数に型がなく、データに型がある

というギャップを埋めるための型です。このVALUE型から文字列を取り出すための関数としてStringValuePtr()が用意されています。
これはRubyのリファレンスマニュアルから見つけました。

次にNODE_TRUEで文書内検索し、NODE_MAIN用の処理を付け加えます。

//compile.c 1270行目付近 変更後
//関数の引数にあるときの処理
    case NODE_TRUE:
        dv = Qtrue;
        break;
    case NODE_FALSE:
        dv = Qfalse;
        break;
    /* 追加ここから */
    case NODE_MAIN:
        dv = (is_orig_progname(node)? Qtrue : Qfalse);
        break;
    /* 追加ここまで */
//compile.c 2380行目付近 変更後
//ifの条件文にあるときの処理
    case NODE_TRUE:
    case NODE_STR:
        /* printf("useless condition eliminate (%s)\n",  ruby_node_name(nd_type(cond))); */
        ADD_INSNL(ret, nd_line(cond), jump, then_label);
        break;
    case NODE_FALSE:
    case NODE_NIL:
        /* printf("useless condition eliminate (%s)\n", ruby_node_name(nd_type(cond))); */
        ADD_INSNL(ret, nd_line(cond), jump, else_label);
        break;
    /* 追加ここから */
    case NODE_MAIN:
        if(is_orig_progname(cond)){
            ADD_INSNL(ret, nd_line(cond), jump, then_label);
        }else{
            ADD_INSNL(ret, nd_line(cond), jump, else_label);
        }
        break;
    /* 追加ここまで */
//compile.c 2929行目付近 変更後
//defined?演算子の後にあるときの処理
    case NODE_TRUE:
        expr_type = DEFINED_TRUE;
        break;
    case NODE_FALSE:
        expr_type = DEFINED_FALSE;
        break;
    /* 追加ここから */
    case NODE_MAIN:
        expr_type = (is_orig_progname(node)? DEFINED_TRUE : DEFINED_FALSE);
        break;
    /* 追加ここまで */
//compile.c 5334行目付近
//上記のどれにも該当しない場合の処理
    case NODE_TRUE:{
        if (!poped) {
            ADD_INSN1(ret, line, putobject, Qtrue);
        }
            break;
    }
    case NODE_FALSE:{
        if (!poped) {
            ADD_INSN1(ret, line, putobject, Qfalse);
        }
            break;
    }
    /* 追加ここから */
    case NODE_MAIN:{
        if (!poped) {
            if(is_orig_progname(node)){
                ADD_INSN1(ret, line, putobject, Qtrue);
            }else{
                ADD_INSN1(ret, line, putobject, Qfalse);
            }
        }
        break;
    }
    /* 追加ここまで */

NODE_TRUENODE_FALSEが処理されている箇所にNODE_MAIN用の場合分けを追加し、 is_orig_progname関数を用いてtrueとfalseどちらになるべきか判断した後、それぞれに合う処理を行うようにしています。

さらにNODE_TRUEでプロジェクト内検索をするとparse.ynode.cでも扱われていることが分かります。
こちらも同様にNODE_MAIN用の処理を追加していきます。

//parse.y 8648行目付近 変更後
//警告"unused literal ignored"を出す
static NODE*
block_append_gen(struct parser_params *parser, NODE *head, NODE *tail)
{
//・・・
      case NODE_TRUE:
      case NODE_FALSE:
      case NODE_MAIN://追加
//parse.c 9407行目付近 変更後
//警告"possibly useless use of"を出す
    case NODE_TRUE:
        useless = "true";
        break;
    case NODE_FALSE:
        useless = "false";
        break;
    /* 追加ここから */
    case NODE_MAIN:
        useless = "__MAIN__";
        break;
    /* 追加ここまで */
//parse.c 9527行目付近 変更後
//ノードが静的か調べる関数?
static int
is_static_content(NODE *node)
{
//・・・
      case NODE_TRUE:
      case NODE_FALSE:
      case NODE_MAIN://追加
//parse.y 9638行目付近 変更後
//条件式としての範囲式
static int
literal_node(NODE *node)
{
//・・・
      case NODE_TRUE:
      case NODE_FALSE:
      case NODE_NIL:
      case NODE_MAIN://追加
//node.c 766行目付近 変更後
//ノードの説明文?
/* 追加ここから */
      case NODE_MAIN:
          ANN("__MAIN__");
          ANN("format: __MAIN__");
          ANN("example: __MAIN__");
          break;
/* 追加ここまで */
//node.c 953行目付近 変更後
//trueやfalseでは何もしない
VALUE
rb_gc_mark_node(NODE *obj)
{
//・・・
    case NODE_TRUE:
    case NODE_FALSE:
    case NODE_MAIN://追加

これでやっとNODE_MAINを追加したことによる辻褄合わせは済んだと思われます。
基本的にNODE_TRUEを真似しただけなので不備があるかもしれません。

さて、ビルドしてテストしてみましょう。
以下のようなテストファイルを用意します。

#hoge.rb
if __MAIN__
    puts "test"
end
puts "a"
#require_hoge.rb
require_relative "hoge"

実際に実行してみると以下のようになります。

$ $HOME/ruby_install/bin/ruby hoge.rb
test
a
$ $HOME/ruby_install/bin/ruby require_hoge.rb
a

ちゃんと動いてますね。
--dump parsetreeオプションをつけて実行してみると今度は2.と異なり、__MAIN__の場所にはNODE_MAINという単一のノードが生成されていることが分かります。
__FILE__$0に置き換えているわけではないので、仮にこれらがRubyに存在しなくても動くということになります。

まとめ

演習「大規模ソフトウェアを手探る」の過程と成果は以上です。
長々と書いてしまいましたが、ここまでで出来たことは
* elsifの代わりにelseifも使えるようにした
* 他のファイルから呼ばれた時にfalse、それ以外のときにtrueになる予約語を追加した

ということだけです。
しかし、その過程でRubyの動作原理や書かれ方を一部とは言え知ることができ、私たちにとっては非常に内容の濃いものでした。
この演習の趣旨である「全容を把握できるわけがない程大きなソフトウェアをいかに扱い、必要な動作を理解し、変更するか」ということも少しは習得できたと思います。

記事の内容に関して、ご指摘等ありましたらコメントを残していただけると幸いです。

suzuhoka.hatenablog.com