以下の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/29/102858より取得しました。


Ruby Parser開発日誌 (24-25) - parse.yが生成するノードを変える ー 文字列

25日目: 文字列に立ち向かう

久しぶりにリテラル周りの対応を進めようかという気持ちになったので、今日は文字列に取り組んでいきます。

シンプルなケース

stringで最もシンプルなケースは次の3つでしょう。

  1. "str"
  2. 'str'
  3. ?c (文字リテラル)

いずれのケースも書き換え前はNODE_STR、書き換え後はStringNodeになります。 クオートの種類や文字リテラルかどうかで特別扱いはしないということですね。

parse.yではset_yylval_strというマクロの中でstringのノードを生成して、tSTRING_CONTENTtCHARといったトークンのsemantic valueにしているため、そのマクロを修正することになります。

 # define set_yylval_str(x) \
 do { \
-  set_yylval_node(NEW_STR(x, &_cur_loc)); \
+  set_yylval_node(NEW_RB_STRING(x, &_cur_loc)); \
   set_parser_s_value(rb_str_new_mutable_parser_string(x)); \
 } while(0)

コンパイラはというとすでに__FILE__のときに文字列をコンパイルするロジックを実装しているので、そこに追加するだけで済みます。

-      case RB_SOURCE_FILE_NODE: {
+      case RB_SOURCE_FILE_NODE:
+      case RB_STRING_NODE: {
         debugp_param("nd_lit", get_string_value2(node));
         if (!popped) {
             VALUE lit = get_string_value2(node);
             const rb_compile_option_t *option = ISEQ_COMPILE_DATA(iseq)->option;
             if ((option->debug_frozen_string_literal || RTEST(ruby_debug)) &&
                 option->frozen_string_literal != ISEQ_FROZEN_STRING_LITERAL_DISABLED) {
                 lit = rb_str_with_debug_created_info(lit, rb_iseq_path(iseq), line);
                 RB_OBJ_SET_SHAREABLE(lit);
             }
             switch (option->frozen_string_literal) {
               case ISEQ_FROZEN_STRING_LITERAL_UNSET:
                 ADD_INSN1(ret, node, putchilledstring, lit);
                 break;
               case ISEQ_FROZEN_STRING_LITERAL_DISABLED:
                 ADD_INSN1(ret, node, putstring, lit);
                 break;
               case ISEQ_FROZEN_STRING_LITERAL_ENABLED:
                 ADD_INSN1(ret, node, putobject, lit);
                 break;
               default:
                 rb_bug("invalid frozen_string_literal");
             }
             RB_OBJ_WRITTEN(iseq, Qundef, lit);
         }
         break;
       }

生成されるノードとバイトコードを確認しておきます。

# @ StringNode (location: (5,1)-(11,10))
# +-- unescaped: "str1"
s1 = "str1"

# @ StringNode (location: (5,2)-(11,10))
# +-- unescaped: "str2"
s2 = 'str2'

# @ StringNode (location: (3,4)-(3,6))
# +-- unescaped: "a"
c = ?a

# 0000 putchilledstring                       "str1"                    (   1)[Li]
# 0002 setlocal_WC_0                          s1@0
# 0004 putchilledstring                       "str2"                    (   2)[Li]
# 0006 setlocal_WC_0                          s2@1
# 0008 putchilledstring                       "a"                       (   3)[Li]
# 0010 dup
# 0011 setlocal_WC_0                          c@2
# 0013 leave

string interpolation

シンプルなケースはシンプルだからいいとして、ここからは複雑なケースを考えていきます。 まずはstring interpolation(式展開)があるケースです。

s1 = "#{var}"というコードを例に生成されるノードを確認しておきましょう。

書き換え前は文字列全体がNODE_DSTR(DはおそらくDynamic)というノードになっていて、その中にNODE_EVSTR(EVはおそらくEValuate)というノードがぶら下がっています。

# @ NODE_DSTR (id: 3, line: 1, location: (1,5)-(1,13))
# +- string: ""
# +- nd_next->nd_head:
# |   @ NODE_EVSTR (id: 2, line: 1, location: (1,5)-(1,13))
# |   +- nd_body:
# |   |   @ NODE_VCALL (id: 1, line: 1, location: (1,8)-(1,11))
# |   |   +- nd_mid: :var
# |   +- opening_loc: (1,6)-(1,8)
# |   +- closing_loc: (1,11)-(1,12)
# +- nd_next->nd_next:
#     (null node)

書き換え後はInterpolatedStringNodeというノードのしたにEmbeddedStatementsNodeというノードがぶら下がっています。

# @ InterpolatedStringNode (location: (1,5)-(1,13))
# +-- InterpolatedStringNodeFlags: nil
# +-- opening_loc: (1,5)-(1,6) = "\""
# +-- parts: (length: 1)
# |   +-- @ EmbeddedStatementsNode (location: (1,6)-(1,12))
# |       +-- opening_loc: (1,6)-(1,8) = "\#{"
# |       +-- statements:
# |       |   @ StatementsNode (location: (1,8)-(1,11))
# |       |   +-- body: (length: 1)
# |       |       +-- @ CallNode (location: (1,8)-(1,11))
# |       |           +-- CallNodeFlags: variable_call, ignore_visibility
# |       |           +-- receiver: nil
# |       |           +-- call_operator_loc: nil
# |       |           +-- name: :var
# |       |           +-- message_loc: (1,8)-(1,11) = "var"
# |       |           +-- opening_loc: nil
# |       |           +-- arguments: nil
# |       |           +-- closing_loc: nil
# |       |           +-- equal_loc: nil
# |       |           +-- block: nil
# |       +-- closing_loc: (1,11)-(1,12) = "}"
# +-- closing_loc: (1,12)-(1,13) = "\""

たまにはちゃんと生成規則を確認しておきましょう。

primary: strings

strings: string

string: tCHAR
      | string1
      | string string1

string1: tSTRING_BEG string_contents tSTRING_END

string生成規則は?c(tCHAR), "str"(string1)などがstringであると定義しています。 string1生成規則はというと、"'tSTRING_BEGtSTRING_ENDであることを踏まえると"str"'str'が含まれることがわかります。

""で挟まれたところに何を書くことができるかというと、それはstring_contentsが定義しています。

string_contents : /* none */
                | string_contents string_content

string_content  : tSTRING_CONTENT
                | tSTRING_DVAR string_dvar
                | tSTRING_DBEG compstmt string_dend

string_contents生成規則は0個以上のstring_content(単数系)が並んでいるということを表しています。 ではstring_contentはというと以下の3つからなります。

  1. 文字列を表すtSTRING_CONTENT
  2. {}を必要としない式展開であるtSTRING_DVAR string_dvar ("#@a""#$a")
  3. #{}による式展開であるtSTRING_DBEG compstmt string_dend

"#{var}"は3番目に該当します。 3番目のケースではnew_evstr関数を呼んでノードを作っています。

string_content | tSTRING_DBEG[state] compstmt(stmts) string_dend
                  {
                      COND_POP();
                      CMDARG_POP();
                      p->lex.strterm = $term;
                      SET_LEX_STATE($state);
                      p->lex.brace_nest = $brace;
                      p->heredoc_indent = $indent;
                      p->heredoc_line_indent = -1;
                      if ($compstmt) nd_unset_fl_newline($compstmt);
                      $$ = new_evstr(p, $compstmt, &@$, &@state, &@string_dend);
                  /*% ripper: string_embexpr!($:compstmt) %*/
                  }
              ;

new_evstr関数では渡されたノードの種類に応じて返すノードの種類が変わります。

static NODE *
new_evstr(struct parser_params *p, NODE *node, const YYLTYPE *loc, const YYLTYPE *opening_loc, const YYLTYPE *closing_loc)
{
    NODE *head = node;

    if (node) {
        switch (nd_type(node)) {
          case NODE_STR:
            return str2dstr(p, node);
          case NODE_DSTR:
            break;
          case NODE_EVSTR:
            return node;
        }
    }
    return NEW_EVSTR(head, loc, opening_loc, closing_loc);
}

たとえばs1 = "#{"str"}"であれば、NODE_DSTR, NODE_EVSTR, NODE_STRというネストした構造にはならず、ひとつのNODE_DSTRからなるノードが生成されます。

s1 = "#{"str"}"

# @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,15))*
# +- nd_vid: :s1
# +- nd_value:
#     @ NODE_DSTR (id: 1, line: 1, location: (1,5)-(1,15))
#     +- string: "str"

ノードの書き換え後はこのようなケースでも入力されたスクリプトの構造を維持して、InterpolatedStringNodeのなかにEmbeddedStatementsNodeがあり、そこにStringNodeがあるという構造をとります。

# @ InterpolatedStringNode (location: (1,5)-(1,15))
# +-- InterpolatedStringNodeFlags: mutable
# +-- opening_loc: (1,5)-(1,6) = "\""
# +-- parts: (length: 1)
# |   +-- @ EmbeddedStatementsNode (location: (1,6)-(1,14))
# |       +-- opening_loc: (1,6)-(1,8) = "\#{"
# |       +-- statements:
# |       |   @ StatementsNode (location: (1,8)-(1,13))
# |       |   +-- body: (length: 1)
# |       |       +-- @ StringNode (location: (1,8)-(1,13))
# |       |           +-- StringFlags: frozen
# |       |           +-- opening_loc: (1,8)-(1,9) = "\""
# |       |           +-- content_loc: (1,9)-(1,12) = "str"
# |       |           +-- closing_loc: (1,12)-(1,13) = "\""
# |       |           +-- unescaped: "str"
# |       +-- closing_loc: (1,13)-(1,14) = "}"

ということでcompstmtのノードの種別によらずEmbeddedStatementsNodeを生成するように変更します。

@@ -6562,8 +6573,8 @@ string_content    : tSTRING_CONTENT
                         p->lex.brace_nest = $brace;
                         p->heredoc_indent = $indent;
                         p->heredoc_line_indent = -1;
-                        if ($compstmt) nd_unset_fl_newline($compstmt);
-                        $$ = new_evstr(p, $compstmt, &@$, &@state, &@string_dend);
+                        if ($compstmt) rb_node_unset_fl_newline($compstmt);
+                        $$ = NEW_RB_EMBEDDED_STATEMENTS($compstmt, &@$, &@state, &@string_dend);
                     /*% ripper: string_embexpr!($:compstmt) %*/
                     }

string_contentのアクションを修正したので、次はstring_contentsです。 string_contentsではliteral_concat関数を呼び出しています。

string_contents    : /* none */
                    {
                        $$ = 0;
                    /*% ripper: string_content! %*/
                    }
                | string_contents string_content
                    {
                        $$ = literal_concat(p, $1, $2, &@$);
                    /*% ripper: string_add!($:1, $:2) %*/
                    }
                ;

literal_concat関数は2つのノードを受け取ります。 ここではheadtailのいずれかがNULLの場合にはNULLでない方を返し、両方がNULLでない場合にはInterpolatedStringNodeを返すようにしておきます。

/* concat two string literals */
static rb_node_t *
literal_concat(struct parser_params *p, rb_node_t *head, rb_node_t *tail, const YYLTYPE *loc)
{
    enum node_type htype;
    rb_parser_string_t *lit;

    if (!head) return tail;
    if (!tail) return head;

    switch (RB_NODE_TYPE(head)) {
      case RB_INTERPOLATED_STRING_NODE:
        rb_node_list_append(&RB_NODE_INTERPOLATED_STRING(head)->parts, tail);
        return head;
      default:
        return NEW_RB_INTERPOLATED_STRING(head, tail, loc);
    }
}

さて、これだけだと"#{var}"のようにstring_contentが1つのときにstringsEmbeddedStatementsNodeになってしまいます。

strings        : string
                    {
                        if (!$1) {
                            $$ = NEW_RB_STRING(STRING_NEW0(), &@$);
                        }
                        else {
                            $$ = evstr2dstr(p, $1);
                        }
                    /*% ripper: $:1 %*/
                    }
                ;

そこでstrings生成規則で呼び出されているevstr2dstr関数を修正して、InterpolatedStringNodeでラップするようにしておきます。

-static NODE *
-evstr2dstr(struct parser_params *p, NODE *node)
+static rb_node_t *
+evstr2dstr(struct parser_params *p, rb_node_t *node)
 {
-    if (nd_type_p(node, NODE_EVSTR)) {
-        node = new_dstr(p, node, &node->nd_loc);
+    if (RB_NODE_TYPE_P(node, RB_EMBEDDED_STATEMENTS_NODE)) {
+        node = NEW_RB_INTERPOLATED_STRING0(node, &node->location);
     }
     return node;
 }

InterpolatedStringNodeコンパイルする

InterpolatedStringNodeEmbeddedStatementsNodeコンパイルできるようにします。

先にバイトコードを見ておきましょう。

# 0000 putobject                              "abc "                    (   1)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 dup
# 0006 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0008 anytostring
# 0009 putobject                              " def"
# 0011 concatstrings                          3
# 0013 leave

"abc #{var} def"

0002から0008までがvar.to_sであると考えると、文字列の断片を全てスタックにのせてconcatstrings N(Nはスタックにのせてある文字列の断片の数)を実行して1つの文字列を作っていることがわかります。

compile.cに進みましょう。 InterpolatedStringNodeはもともとはNODE_DSTRだったので、NODE_DSTRがどのようにコンパイルされていたのか眺めます。

// iseq_compile_each0
      case NODE_DSTR:{
        compile_dstr(iseq, ret, node);

        if (popped) {
            ADD_INSN(ret, node, pop);
        }
        break;
      }

static int
compile_dstr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node)
{
    int cnt;
    if (!RNODE_DSTR(node)->nd_next) {
        VALUE lit = rb_node_dstr_string_val(node);
        ADD_INSN1(ret, node, putstring, lit);
        RB_OBJ_SET_SHAREABLE(lit);
        RB_OBJ_WRITTEN(iseq, Qundef, lit);
    }
    else {
        CHECK(compile_dstr_fragments(iseq, ret, node, &cnt, FALSE));
        ADD_INSN1(ret, node, concatstrings, INT2FIX(cnt));
    }
    return COMPILE_OK;
}

iseq_compile_each0関数はcompile_dstr関数を呼び出すだけです。 compile_dstr関数はというと、nd_nextがないとき、つまり文字列の要素が1つのときはその文字列をオペランドとするputstring命令を生成します。 それ以外のケースではcompile_dstr_fragments関数に処理を移譲しています。

nd_nextがないときの処理はNODE_DSTRの先頭の要素が必ずNODE_STRになっていたことを踏まえた処理でしょう。 parse.yの変更でInterpolatedStringNodeがただ一つのEmbeddedStatementsNodeを持つこともあるようになったので、nd_nextがないときの処理は消しておくことにします。

compile_dstr_fragments関数はcompile_dstr_fragments_0関数を呼び出したのち、flush_dstr_fragment関数を呼びます。

static int
compile_dstr_fragments(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int *cntp, int dregx)
{
    struct dstr_ctxt args = {
        .iseq = iseq, .ret = ret,
        .lit = Qnil, .lit_node = NULL,
        .cnt = 0, .dregx = dregx,
    };
    CHECK(compile_dstr_fragments_0(&args, node));
    flush_dstr_fragment(&args);

    *cntp = args.cnt;

    return COMPILE_OK;
}

InterpolatedStringNodeコンパイルするメインの関数がcompile_dstr_fragments_0関数です。 whileのループに注目すると、この関数はInterpolatedStringNodeの持っている文字列の断片たちを1つずつ処理していることがわかります。 もしその断片がStringのノードであればappend_dstr_fragment関数を呼んでバッファに追加します。 またstring interpolation(NODE_DSTR)であればcompile_dstr_fragments_0関数を再帰的に呼び出しています。 その他のノードであればCOMPILEに渡してコンパイルします。 #{var}を表すNODE_EVSTRは最後のケースに該当するのでiseq_compile_eachで処理することになります。

concatstrings NのNを決めるのに必要な情報などはstruct dstr_ctxt *argsで管理しており、呼び出し元のcompile_dstr関数でconcatstrings命令を挿入するときにargsの情報を参照してNを決定しています。

static int
compile_dstr_fragments_0(struct dstr_ctxt *args, const NODE *const node)
{
    const struct RNode_LIST *list = RNODE_DSTR(node)->nd_next;
    rb_parser_string_t *str = RNODE_DSTR(node)->string;

    if (str) {
        CHECK(append_dstr_fragment(args, node, str));
    }

    while (list) {
        const NODE *const head = list->nd_head;
        if (nd_type_p(head, NODE_STR)) {
            CHECK(append_dstr_fragment(args, node, RNODE_STR(head)->string));
        }
        else if (nd_type_p(head, NODE_DSTR)) {
            CHECK(compile_dstr_fragments_0(args, head));
        }
        else {
            flush_dstr_fragment(args);
            rb_iseq_t *iseq = args->iseq;
            CHECK(COMPILE(args->ret, "each string", head));
            args->cnt++;
        }
        list = (struct RNode_LIST *)list->nd_next;
    }
    return COMPILE_OK;
}

InterpolatedStringNodeが配列としてノードを保持していることや、NODE_DSTRと異なり最初の要素が必ずしもStringNodeとは限らないことなどを加味すると、compile_dstr_fragments_0関数は以下のように書き換えることができます。

static int
compile_dstr_fragments_0(struct dstr_ctxt *args, const NODE *const node)
{
    const rb_node_list2_t *list = &RB_NODE_INTERPOLATED_STRING(node)->parts;

    for (size_t i = 0; i < RB_NODE_LIST_LEN(list); i++) {
        const NODE *const head = list->nodes[i];

        if (nd_type_p(head, RB_STRING_NODE)) {
            CHECK(append_dstr_fragment(args, node, RB_NODE_STRING(head)->unescaped));
        }
        else if (nd_type_p(head, RB_INTERPOLATED_STRING_NODE)) {
            CHECK(compile_dstr_fragments_0(args, head));
        }
        else {
            flush_dstr_fragment(args);
            rb_iseq_t *iseq = args->iseq;
            CHECK(COMPILE(args->ret, "each string", head));
            args->cnt++;
        }
    }

    return COMPILE_OK;
}

EmbeddedStatementsNodeコンパイルする

#{var}の部分のコンパイル結果はvarの部分とNODE_EVSTR固有の部分に分けられます。

# varの部分
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>

# NODE_EVSTR固有の部分
# 0005 dup
# 0006 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0008 anytostring

"abc #{var} def" # のうちの #{var} の部分

EmbeddedStatementsNodeコンパイルNODE_EVSTRコンパイルを参考にしましょう。 NODE_EVSTRコンパイルcompile_evstr関数に移譲されていました。

// iseq_compile_each0
case NODE_EVSTR:
  CHECK(compile_evstr(iseq, ret, RNODE_EVSTR(node)->nd_body, popped));
  break;

compile_evstr関数は特にNODE_EVSTRに依存した処理は入っていないのでそのまま使うことができます。

static int
compile_evstr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    CHECK(COMPILE_(ret, "nd_body", node, popped));

    if (!popped && !all_string_result_p(node)) {
        const NODE *line_node = node;
        const unsigned int flag = VM_CALL_FCALL;

        // Note, this dup could be removed if we are willing to change anytostring. It pops
        // two VALUEs off the stack when it could work by replacing the top most VALUE.
        ADD_INSN(ret, line_node, dup);
        ADD_INSN1(ret, line_node, objtostring, new_callinfo(iseq, idTo_s, 0, flag, NULL, FALSE));
        ADD_INSN(ret, line_node, anytostring);
    }
    return COMPILE_OK;
}

ということでiseq_compile_each0関数にEmbeddedStatementsNodeの分岐を追加しておしまいです。

// iseq_compile_each0
case RB_EMBEDDED_STATEMENTS_NODE: {
  CHECK(compile_evstr(iseq, ret, RB_NODE_EMBEDDED_STATEMENTS(node)->statements, popped));
  break;
}

試し文字列を表示してみましょう。

var = "World"
p "Hello, #{var}!"
#=> "Hello, World!"

25日目にしてはじめてHello worldができました。 めでたい。

ほかにもいくつか試してみましょう。

p ""
#=> ""

p "#{true}"
#=> "true"

p "#{"str"}"
#=> "str"

もう一つの式展開をサポートする

#{}による式展開とは別に、{}を必要としない式展開("#@a""#$a")もあるので、そちらも対応しておきましょう。

書き換え前は#{}と同様にNODE_EVSTRを使っていて、その下にNODE_IVARNODE_GVARといったノードがぶら下がっていたのですが、書き換え後はEmbeddedVariableNodeという専用のノードが割り当てられています。

"#@a #$b"

# Before
#
#     @ NODE_DSTR (id: 3, line: 1, location: (1,0)-(1,9))*
#     +- string: ""
#     +- nd_next->nd_head:
#     |   @ NODE_EVSTR (id: 1, line: 1, location: (1,1)-(1,4))
#     |   +- nd_body:
#     |   |   @ NODE_IVAR (id: 0, line: 1, location: (1,2)-(1,4))
#     |   |   +- nd_vid: :@a
#     |   +- opening_loc: (1,1)-(1,2)
#     |   +- closing_loc: (0,-1)-(0,-1)
#     +- nd_next->nd_next:
#         @ NODE_LIST (id: 5, line: 1, location: (1,4)-(1,5))
#         +- as.nd_alen: 1
#         +- nd_head:
#         |   @ NODE_STR (id: 2, line: 1, location: (1,4)-(1,5))
#         |   +- string: " "
#         +- nd_head:
#         |   @ NODE_EVSTR (id: 7, line: 1, location: (1,5)-(1,8))
#         |   +- nd_body:
#         |   |   @ NODE_GVAR (id: 6, line: 1, location: (1,6)-(1,8))
#         |   |   +- nd_vid: :$b
#         |   +- opening_loc: (1,5)-(1,6)
#         |   +- closing_loc: (0,-1)-(0,-1)
#         +- nd_next:
#             (null node)

# After
#
# +-- @ EmbeddedVariableNode (location: (1,1)-(1,4))
# |   +-- operator_loc: (1,1)-(1,2) = "#"
# |   +-- variable:
# |       @ InstanceVariableReadNode (location: (1,2)-(1,4))
# |       +-- name: :@a
# +-- @ StringNode (location: (1,4)-(1,5))
# |   +-- StringFlags: frozen
# |   +-- opening_loc: nil
# |   +-- content_loc: (1,4)-(1,5) = " "
# |   +-- closing_loc: nil
# |   +-- unescaped: " "
# +-- @ EmbeddedVariableNode (location: (1,5)-(1,8))
#     +-- operator_loc: (1,5)-(1,6) = "#"
#     +-- variable:
#         @ GlobalVariableReadNode (location: (1,6)-(1,8))
#         +-- name: :$b

parse.yの変更は呼び出すマクロの修正だけです。

@@ -6532,7 +6544,7 @@ string_content    : tSTRING_CONTENT
                   string_dvar
                     {
                         p->lex.strterm = $2;
-                        $$ = NEW_EVSTR($3, &@$, &@1, &NULL_LOC);
+                        $$ = NEW_RB_EMBEDDED_VARIABLE($3, &@$, &@1);
                         nd_set_line($$, @3.end_pos.lineno);
                     /*% ripper: string_dvar!($:3) %*/
                     }

このノードのコンパイルはもともとNODE_EVSTRだったことを考えると、EmbeddedStatementsNodeコンパイルとおなじで問題ないでしょう。

// iseq_compile_each0
case RB_EMBEDDED_VARIABLE_NODE: {
  CHECK(compile_evstr(iseq, ret, RB_NODE_EMBEDDED_VARIABLE(node)->variable, popped));
  break;
}

実行してみましょう。

@a = "!"
$b = "?"

p "#@a #$b"
#=> "! ?"

よさそうですね。

おまけ: all_string_result_p関数

式展開がある場合は式を評価したあとにdupをしてobjtostring命令とanytostring命令を実行して評価結果を文字列にします。

"#{var}"

# 0000 putobject                              ""                        (   1)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 dup
# 0006 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0008 anytostring
# 0009 concatstrings                          2
# 0011 leave

もし式の評価結果が文字列であることが事前にわかるのであればdup, objtostring, anytostring命令を生成・実行する必要はなくなります。

"#{ cond ? "t" : "f" }"

# 0000 putobject                              ""                        (   1)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 branchunless                           11
# 0007 putchilledstring                       "t"
# 0009 jump                                   13
# 0011 putchilledstring                       "f"
# 0013 concatstrings                          2
# 0015 leave

compile_evstr関数はall_string_result_p関数を用いてこの判定を行っています。

static int
compile_evstr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    CHECK(COMPILE_(ret, "nd_body", node, popped));

    if (!popped && !all_string_result_p(node)) {
        const NODE *line_node = node;
        const unsigned int flag = VM_CALL_FCALL;

        // Note, this dup could be removed if we are willing to change anytostring. It pops
        // two VALUEs off the stack when it could work by replacing the top most VALUE.
        ADD_INSN(ret, line_node, dup);
        ADD_INSN1(ret, line_node, objtostring, new_callinfo(iseq, idTo_s, 0, flag, NULL, FALSE));
        ADD_INSN(ret, line_node, anytostring);
    }
    return COMPILE_OK;
}

static int
all_string_result_p(const NODE *node)
{
    if (!node) return FALSE;
    switch (nd_type(node)) {
      case RB_STRING_NODE: case RB_INTERPOLATED_STRING_NODE: case RB_SOURCE_FILE_NODE:
        return TRUE;
      case RB_IF_NODE: case RB_UNLESS_NODE:
        if (!RB_NODE_IF(node)->statements || !RB_NODE_IF(node)->subsequent) return FALSE;
        if (all_string_result_p(RB_NODE_IF(node)->statements))
            return all_string_result_p(RB_NODE_IF(node)->subsequent);
        return FALSE;
      case RB_AND_NODE: case RB_OR_NODE:
        if (!RB_NODE_AND(node)->right)
            return all_string_result_p(RB_NODE_AND(node)->left);
        if (!all_string_result_p(RB_NODE_AND(node)->left))
            return FALSE;
        return all_string_result_p(RB_NODE_AND(node)->right);
      default:
        return FALSE;
    }
}

ところで現在の手元の実装では、この最適化が一部で効かなくなっています。

"#{ cond ? "t" : "f" }"

# 0000 putself                                                          (   1)[Li]
# 0001 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0003 branchunless                           9
# 0005 putchilledstring                       "t"
# 0007 jump                                   11
# 0009 putchilledstring                       "f"
# 0011 dup
# 0012 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0014 anytostring
# 0015 leave

これはIfNodeの子ノードの種類が変わったためです。 もともとはthenとelseの中身が1行のときはNODE_STRなどが直接NODE_IFにぶら下がっていました。 これがノードの書き換えで常にStatementsNodeがぶら下がるようになったためall_string_result_p関数の条件を満たさなくなっています。 また、elseの子ノードは常にElseNodeになったことも原因です。

それぞれ対応するようにall_string_result_p関数に分岐を追加しておきましょう1

@@ -4867,14 +4867,14 @@ all_string_result_p(const NODE *node)
         if (!all_string_result_p(RB_NODE_AND(node)->left))
             return FALSE;
         return all_string_result_p(RB_NODE_AND(node)->right);
+      case RB_STATEMENTS_NODE: {
+        const rb_node_list2_t *list = &RB_NODE_STATEMENTS(node)->body;
+        if (RB_NODE_LIST_LEN(list) == 1)
+            return all_string_result_p(list->nodes[0]);
+        return FALSE;
+      }
+      case RB_ELSE_NODE:
+        return all_string_result_p(RB_NODE_ELSE(node)->statements);
       default:
         return FALSE;
     }

dup, objtostring, anytostring命令が消えていることがわかります2

"#{ cond ? "t" : "f" }"

# 0000 putself                                                          (   1)[Li]
# 0001 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0003 branchunless                           9
# 0005 putchilledstring                       "t"
# 0007 jump                                   11
# 0009 putchilledstring                       "f"
# 0011 concatstrings                          1
# 0013 leave                                                            (   1)

まとめ

今日の成果です。

文字列関係でやるべきことは多く、まだ以下のような機能に対応していません。 次回も引き続き文字列に取り組みたいと思います。

  • 文字列の結合
  • ヒアドキュメント
  • %記法

  1. 極めて限定的な範囲の最適化で、caseに対応していないですし、複数行あってそれが全部文字列のケースにも対応していません。なので正直最適化ごと消すという方法もあるのですが、後で生成されるバイトコードのdiffを確認する予定なので、そのときにdiffが小さくなるように最適化を残しておくことにします。
  2. ノードの変更前は0000 putobject ""という命令が入っています。この辺の微調整はあとで行うかもしれません。



以上の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/29/102858より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14