プログラム言語Rubyにはunlessという構文があります。これはif文の反対で与えられた条件式が偽の場合に処理を行います。
unless cond then print 'OK' end #上記の場合、condがFalseならOKが表示されます
この記事ではPythonに上記のようなunless文を実装した結果を紹介します。
文法定義をいじる
ビルドまでの経緯はこちら
Pythonの公式ドキュメントに文法変更のガイドラインがあるので、これに沿って改造を施していきます。
まず、ソースファイル内のGrammar/Grammarを書き換えてunless文の定義を追加します。
GrammarファイルはPythonの文法を規定したテキストファイルです。
Grammarは独自の記法で書かれていましたが、unless文の文法構造はif文とほとんど同じなので、if文を真似れば問題なさそうです。
# #if IF_INV
compound_stmt: if_stmt | unless_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
async_stmt: ASYNC (funcdef | with_stmt | for_stmt)
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
# #if IF_INV
unless_stmt: 'unless' test ':' suite ('elunless' test ':' suite)* ['else' ':' suite]
if文には、else ifの省略形であるelifという構文が実装されています。unless文にもとりあえずこれに対応するelunlessという構文を追加しました。なお、unless文の元ネタであるRubyにはelsifに対応するelsunlessは存在しません。今回は細かいことは気にせずにif文の実装を完全コピーしました。(実際にelunlessを使うとコードが読みにくくなるので実用性はありませんが…)
Grammarファイルでは#以降の文字列は無視されるようなので、変更した部分を検索しやすいように#if IF_INVという文字列を置いておきました。
ちなみに、Grammar内にascii文字以外の文字を書くと、たとえコメントアウトされている部分でもエラーが発生するため、日本語のコメントは書けないようです。
次にPaser/Python.asdlを書き換えます。このファイルはMake時の文法解析プログラムの自走生成に使われます。
| If(expr test, stmt* body, stmt* orelse) -- #if IF_INV | Unless(expr test, stmt* body, stmt* orelse)
以上の2つのファイルの変更後、自動生成ファイルを書き換えてもらうために一度makeを行いました。makeは失敗しましたが(unless文が存在するが動作が定義されていないみたいなエラーが出た)自動生成ファイルの書き換えが行われました。
//Python-ast.c static PyTypeObject *If_type; static char *If_fields[]={ "test", "body", "orelse", }; //unless文に対応する部分が追加された static PyTypeObject *Unless_type; static char *Unless_fields[]={ "test", "body", "orelse", };
構文木、記号表を作り変える
Pythonは与えられた構文からAST(抽象構文木)を作成します。構文木生成プログラムはPython/ast.cに記載されていますが、このファイルはmake時に自動で書き換えてくれないので手動で書き換えます。
ast.cにunless文処理用の関数を追加します。
#if IF_INV static stmt_ty ast_for_unless_stmt(struct compiling *c, const node *n) { /* unless_stmt: 'unless' test ':' suite ('elunless' test ':' suite)* ['else' ':' suite] */ char *s; //if文処理用の関数をコピーしifをunlessに書き換える REQ(n, unless_stmt); : : //elseとelifを区別する処理 //elifの部分をelunlessに対応させる s = STR(CHILD(n, 4)); /* s[2], the third character in the string, will be 's' for el_s_e, or 'u' for el_u_nless */ if (s[2] == 's') { : : } else if (s[2] == 'u') { : : } : : } #endif
書き換えた部分には#if IF_INVというフラグを置いておき、コンパイルオプションでOn/Offを切り替えられるようにしておきました。 コンパイルオプションで-DIF_INV=1を指定すると変更部分が反映されます。
Python/symtable.cも書き換えます。symtable.cは記号表(抽象構文木に意味付けするときに使われる?)を作るプログラムです。これもif文の真似をしました。
case If_kind: /* XXX if 0: and lookup_yield() hacks */ VISIT(st, expr, s->v.If.test); VISIT_SEQ(st, stmt, s->v.If.body); if (s->v.If.orelse) VISIT_SEQ(st, stmt, s->v.If.orelse); break; #if IF_INV case Unless_kind: VISIT(st, expr, s->v.Unless.test); VISIT_SEQ(st, stmt,s->v.Unless.body); if (s->v.Unless.orelse) VISIT_SEQ(st, stmt, s->v.Unless.body); break; #endif
コンパイラを改造
ast.cとsymtable.cを書き換えたことでunless文が正しく分解されるようになりました。今度は分解された文に沿って正しいオペコードを生成するようにPython/comlipe.cを書き換えます。
これも真偽値を反転させること以外はif文の実装部分と同じなので真似して書きます。
#if IF_INV static int compiler_unless(struct compiler *c, stmt_ty s) { : : constant = expr_constant(c, s->v.Unless.test); /* constant = 0: "if 0" * constant = 1: "if 1", "if 2", ... * constant = -1: rest */ if (constant == 1) {//0と1を入れ替え if (s->v.Unless.orelse) VISIT_SEQ(c, stmt, s->v.Unless.orelse); } else if (constant == 0) {//0と1を入れ替え VISIT_SEQ(c, stmt, s->v.Unless.body); } else { if (asdl_seq_LEN(s->v.Unless.orelse)) { next = compiler_new_block(c); if (next == NULL) return 0; } else next = end; VISIT(c, expr, s->v.Unless.test); //生成するオペコードの真偽を入れ替え ADDOP_JABS(c, POP_JUMP_IF_TRUE, next); VISIT_SEQ(c, stmt, s->v.Unless.body); if (asdl_seq_LEN(s->v.Unless.orelse)) { ADDOP_JREL(c, JUMP_FORWARD, end); compiler_use_next_block(c, next); VISIT_SEQ(c, stmt, s->v.Unless.orelse); } } compiler_use_next_block(c, end); return 1; } #endif
実際に使ってみる
以上でunless文が実装できた。早速ビルドしてテストしてみます。
$ CFLAGS="-O0 -g -DIF_INV=1" ./configure --prefix='/home/pf-siedler/mypython/' #コンパイルオプションに -DIF_INV=1 を追加 $ make -j8 $ make install
実際にunless文を実行してみましょう。
>>> unless False: ... print("OK") ... OK >>> unless True: ... print("NO") ... elunless False: ... print("YES") ... else: ... print("NO'") ... YES >>> unless True: ... print("NO") ... else: ... print("DONE!") ... DONE! >>>
ちゃんと条件式が偽のときに内部の命令が実行されました!
次にdisassembleモジュールでunless文のオペコードを見てみましょう。
>>> import dis >>> def f(a): ... unless a: ... print("OK") ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (a) 3 POP_JUMP_IF_TRUE 16 3 6 LOAD_GLOBAL 0 (print) 9 LOAD_CONST 1 ('OK') 12 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 15 POP_TOP >> 16 LOAD_CONST 0 (None) 19 RETURN_VALUE >>>
3行目POP_JUMP_IF_TRUEは直前にロードした変数がTrueなら16行目に移動(つまりunless文から抜ける)、
Falseなら次の行(unless文の中身)を実行という処理で、正しくオペコードが生成されていることがわかります。
まとめ
CPythonはGrammarやPython.asdlで文法を規定し、ast.c、symtable.cに沿って構文木を作成、構文木に沿ってcompile.cがオペコードを生成していることがわかりました。
既存の構文に似た予約語の追加は、既存コードを少し改造するだけで実装できるので簡単に行なえます。
より複雑な構文を実装する場合、正しくオペコードを吐き出すようにcompile.cの改造に工夫を凝らす必要がありそうです。