読者です 読者をやめる 読者になる 読者になる

開拓馬’s blog

いろいろやる

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.cunless文処理用の関数を追加します。

#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.csymtable.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はGrammarPython.asdlで文法を規定し、ast.csymtable.cに沿って構文木を作成、構文木に沿ってcompile.cがオペコードを生成していることがわかりました。

既存の構文に似た予約語の追加は、既存コードを少し改造するだけで実装できるので簡単に行なえます。

より複雑な構文を実装する場合、正しくオペコードを吐き出すようにcompile.cの改造に工夫を凝らす必要がありそうです。