27日目: xstringと式展開を含んだsymbolに対応する
前回は%記法、文字列の結合、ヒアドキュメントなどの対応をしました。
今回は`cmd`のような文字列(通称xstring)と式展開を含んだsymbolに取り組みたいと思います。
parserを変更する
xstringに関連する生成規則をみていきましょう。
// '`' cmd '`' を表す xstring : tXSTRING_BEG xstring_contents tSTRING_END { $$ = new_xstring(p, heredoc_dedent(p, $2), &@$); /*% ripper: $:2 %*/ if (p->heredoc_indent > 0) { /*% ripper: heredoc_dedent!($:$, INT2NUM(%{p->heredoc_indent})) %*/ p->heredoc_indent = 0; } /*% ripper: xstring_literal!($:$) %*/ } ; // cmd や #{var} などを表す xstring_contents: /* none */ { $$ = 0; /*% ripper: xstring_new! %*/ } | xstring_contents string_content { $$ = literal_concat(p, $1, $2, &@$); /*% ripper: xstring_add!($:1, $:2) %*/ } ;
なんだか文字列のときと同じような生成規則、同じようなアクションになっています。
new_xstring関数はというと、xstring_contentsの生成するノードの種類に応じてそれらをxstring用のノードに変換しています。
static NODE * new_xstring(struct parser_params *p, NODE *node, const YYLTYPE *loc) { if (!node) { NODE *xstr = NEW_XSTR(STRING_NEW0(), loc); return xstr; } switch (nd_type(node)) { case NODE_STR: nd_set_type(node, NODE_XSTR); nd_set_loc(node, loc); break; case NODE_DSTR: nd_set_type(node, NODE_DXSTR); nd_set_loc(node, loc); break; default: node = NEW_DXSTR(0, 1, NEW_LIST(node, loc), loc); break; } return node; }
nd_set_typeでノードのtypeだけを変更しているのは、NODE_STRとNODE_XSTRやNODE_DSTRとNODE_DXSTRが同じ構造をしているからです。
書き換え後でいうと、StringNodeとXStringNode、InterpolatedStringNodeとInterpolatedXStringNodeは同じ構造なので引き続きtypeだけを変更するようにしておきましょう。
static rb_node_t * new_xstring(struct parser_params *p, rb_node_t *node, const YYLTYPE *loc) { if (!node) { rb_node_t *xstr = NEW_RB_X_STRING(STRING_NEW0(), loc); return xstr; } switch (nd_type(node)) { case RB_STRING_NODE: rb_nd_set_type(node, RB_X_STRING_NODE); rb_nd_set_loc(node, loc); break; case RB_INTERPOLATED_STRING_NODE: rb_nd_set_type(node, RB_INTERPOLATED_X_STRING_NODE); rb_nd_set_loc(node, loc); break; default: node = NEW_RB_INTERPOLATED_X_STRING(node, loc); break; } return node; }
compilerを変更する
compile.cはというと文字列をスタックに積んで`メソッドを呼びだすようなバイトコードを生成します。
case NODE_XSTR:{ ADD_CALL_RECEIVER(ret, node); VALUE str = rb_node_str_string_val(node); ADD_INSN1(ret, node, putobject, str); RB_OBJ_WRITTEN(iseq, Qundef, str); ADD_CALL(ret, node, idBackquote, INT2FIX(1)); if (popped) { ADD_INSN(ret, node, pop); } break; } case NODE_DXSTR:{ ADD_CALL_RECEIVER(ret, node); compile_dstr(iseq, ret, node); ADD_CALL(ret, node, idBackquote, INT2FIX(1)); if (popped) { ADD_INSN(ret, node, pop); } break; }
特に難しいことはないので素直に書き換えます。
case RB_X_STRING_NODE: { ADD_CALL_RECEIVER(ret, node); VALUE str = rb_node_str_string_val2(node); ADD_INSN1(ret, node, putobject, str); RB_OBJ_WRITTEN(iseq, Qundef, str); ADD_CALL(ret, node, idBackquote, INT2FIX(1)); if (popped) { ADD_INSN(ret, node, pop); } break; } case RB_INTERPOLATED_X_STRING_NODE: { ADD_CALL_RECEIVER(ret, node); compile_dstr(iseq, ret, node); ADD_CALL(ret, node, idBackquote, INT2FIX(1)); if (popped) { ADD_INSN(ret, node, pop); } break; }
生成されるノードとバイトコードを確認しておきます。 よさそうですね。
# +-- @ XStringNode (location: (1,0)-(1,5))* # | +-- unescaped: "cmd" # # 0000 putself ( 1)[Li] # 0001 putobject "cmd" # 0003 opt_send_without_block <calldata!mid:`, argc:1, FCALL|ARGS_SIMPLE> # 0005 pop `cmd` # +-- @ InterpolatedXStringNode (location: (2,0)-(2,14))* # | +-- parts: (length: 3) # | | +-- @ StringNode (location: (2,1)-(2,4)) # | | | +-- unescaped: "cmd" # | | +-- @ EmbeddedStatementsNode (location: (2,4)-(2,10)) # | | +-- @ StringNode (location: (2,10)-(2,13)) # | | +-- unescaped: "cmd" # # 0006 putself ( 2)[Li] # 0007 putobject "cmd" # 0009 putself # 0010 opt_send_without_block <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0012 dup # 0013 objtostring <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE> # 0015 anytostring # 0016 putobject "cmd" # 0018 concatstrings 3 # 0020 opt_send_without_block <calldata!mid:`, argc:1, FCALL|ARGS_SIMPLE> # 0022 pop `cmd#{var}cmd` # +-- @ XStringNode (location: (3,0)-(3,2))* # +-- unescaped: "" # # 0023 putself ( 3)[Li] # 0024 putobject "" # 0026 opt_send_without_block <calldata!mid:`, argc:1, FCALL|ARGS_SIMPLE> # 0028 leave ``
式展開を含んだsymbolに対応する
xstringの対応がさくっと終わったので、式展開を含んだsymbolもやってしまいましょう。 式展開を含んだsymbolというのは例えば以下のような書き方で作ることができます。
var = "b" p :"a#{var}c" #=> :abc
関連する生成規則とそこで呼ばれている関数の定義は以下のとおりです。 いままで見てきたstringやxstringと同じような構造になっていることがわかります。
dsym : tSYMBEG string_contents tSTRING_END { SET_LEX_STATE(EXPR_END); $$ = dsym_node(p, $2, &@$); /*% ripper: dyna_symbol!($:2) %*/ } ; static NODE* dsym_node(struct parser_params *p, NODE *node, const YYLTYPE *loc) { if (!node) { return NEW_RB_SYMBOL(STR_NEW0(), loc); } switch (nd_type(node)) { case NODE_DSTR: nd_set_type(node, NODE_DSYM); nd_set_loc(node, loc); break; case NODE_STR: node = str_to_sym_node(p, node, loc); break; default: node = NEW_DSYM(0, 1, NEW_LIST(node, loc), loc); break; } return node; }
dsym_node関数はいままでと同じように書き換えればよいでしょう。
static rb_node_t* dsym_node(struct parser_params *p, rb_node_t *node, const YYLTYPE *loc) { if (!node) { return NEW_RB_SYMBOL(STR_NEW0(), loc); } switch (nd_type(node)) { case RB_INTERPOLATED_STRING_NODE: rb_nd_set_type(node, RB_INTERPOLATED_SYMBOL_NODE); nd_set_loc(node, loc); break; case RB_STRING_NODE: node = str_to_sym_node(p, node, loc); break; default: node = NEW_RB_INTERPOLATED_SYMBOL(node, loc); break; } return node; }
dsymをコンパイルする
compile.cでは式を評価してintern命令を実行するようなバイトコードを生成します。
これもノードの書き換え前後で大きく変わらないのでさくっと書き換えます。
// Before case NODE_DSYM:{ compile_dstr(iseq, ret, node); if (!popped) { ADD_INSN(ret, node, intern); } else { ADD_INSN(ret, node, pop); } break; } // After case RB_INTERPOLATED_SYMBOL_NODE: { compile_dstr(iseq, ret, node); if (!popped) { ADD_INSN(ret, node, intern); } else { ADD_INSN(ret, node, pop); } break; }
コードを実行して問題なく動くことを確認します。
var = "b" p :"sym" #=> :sym p :"a#{var}c" #=> :abc p :"" #=> :""
dsymが引数のキーになっているとき
parse.yでdsym_node関数を呼び出しているのはdsym生成規則だけではありません。
assoc生成規則のなかにもdsym_node関数を呼び出している規則があります1。
assoc | tSTRING_BEG string_contents tLABEL_END arg_value
{
YYLTYPE loc = code_loc_gen(&@1, &@3);
$$ = NEW_RB_ASSOC(dsym_node(p, $2, &loc), $4, &@$, &NULL_LOC);
/*% ripper: assoc_new!(dyna_symbol!($:2), $:4) %*/
}
これは例えばメソッドの引数のキーとしてdsymを書くことができるということを表しています。
def m(abc: nil) p abc end var = "b" m("a#{var}c": true) #=> true
このようなケースでも期待した通りのノードとバイトコードが生成されていることを確認しておきます。
def m(abc: nil) p abc end var = "b" # @ CallNode (location: (6,6)-(19,1))* # +-- receiver: nil # +-- name: :m # +-- arguments: # | @ ArgumentsNode (location: (6,2)-(6,18)) # | +-- ArgumentsNodeFlags: nil # | +-- arguments: (length: 1) # | +-- @ KeywordHashNode (location: (6,2)-(6,18)) # | +-- KeywordHashNodeFlags: nil # | +-- elements: (length: 1) # | +-- @ AssocNode (location: (6,2)-(6,18)) # | +-- key: # | | @ InterpolatedSymbolNode (location: (2,6)-(13,10)) # | | +-- parts: (length: 3) # | | | +-- @ StringNode (location: (6,3)-(6,4)) # | | | | +-- unescaped: "a" # | | | +-- @ EmbeddedStatementsNode (location: (6,4)-(6,10)) # | | | +-- @ StringNode (location: (6,10)-(6,11)) # | | | +-- unescaped: "c" # | +-- value: # | | @ TrueNode (location: (6,14)-(6,18)) # # 0007 putself ( 6)[Li] # 0008 putobject "a" # 0010 getlocal_WC_0 var@0 # 0012 dup # 0013 objtostring <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE> # 0015 anytostring # 0016 putobject "c" # 0018 concatstrings 3 # 0020 intern # 0021 putobject true # 0023 newhash 2 # 0025 opt_send_without_block <calldata!mid:m, argc:1, FCALL|KW_SPLAT> # 0027 leave m("a#{var}c": true)
まとめ
今日の成果です。
- xstringの対応をした
- 式展開を含むsymbol(dsym)の対応をした
-
p_kw_label | tSTRING_BEG string_contents tLABEL_ENDというパターンマッチングに関連する生成規則でもdsym_node関数を呼び出していますが、これについてはパターンマッチングのときに見ることにします。↩