Pythonを改造してみた unless文を追加してみた
プログラム言語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
の改造に工夫を凝らす必要がありそうです。