Pythonにunless文を追加する

※このブログはeeicの実験「大規模ソフトウェアを手探る」のレポートとして書かれたものです。

リンク

導入

実験をするにあたって、まずは先輩のunless文を追加してみたのレポートを参考にすることにしました。しかしPythonのバージョンの違いによりうまく行きませんでした。 実験終了後、途中まで手探ったunless文を今なら実装できるのではと思いいじってみたらうまくいったのでおまけとして記録しておきたいと思います。

流れ

Pythonコンパイルの流れは上記の公式ドキュメントに記されています。簡単に書くと以下の通りです。

ソースコード
↓ Parser/tokenizer.c
トークン列
↓ Parser/parser.c
AST(抽象構文木)
↓ Python/compile.c
CFG(制御フローグラフ)
↓ Python/compile.c
バイトコード(Python仮想マシンが読み取るアセンブリ言語のようなもの)

ここで、ソースコードからASTへの変換をいじるには、Grammar/python.gramGrammar/Tokensに変更を加え、make regen-tokenmake regen-pegenコマンドで関連ファイルを書き換えます(PythonにC言語っぽい文法を追加するを参照)。また、ASTからバイトコードへの変換はPython/compile.cに変更を加えればよさそうです。

パーサの変更

PythonにC言語っぽい文法を追加するでも書いたとおり、Grammar/python.gramを変更してunlessを解析できるようにします。ここではifの定義を参考にしたいので、ファイル内検索でif文に関連した部分を探しました。

Grammar/python.gram

compound_stmt[stmt_ty]:
    | &('def' | '@' | ASYNC) function_def
    | &'if' if_stmt
    | &'unless' unless_stmt
    | &('class' | '@') class_def
    | &('with' | ASYNC) with_stmt
    | &('for' | ASYNC) for_stmt
    | &'try' try_stmt
    | &'while' while_stmt
    
(省略)

if_stmt[stmt_ty]:
    | 'if' a=named_expression ':' b=block c=elif_stmt { _Py_If(a, b, CHECK((asdl_stmt_seq*)_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'if' a=named_expression ':' b=block c=[else_block] { _Py_If(a, b, c, EXTRA) }
elif_stmt[stmt_ty]:
    | 'elif' a=named_expression ':' b=block c=elif_stmt { _Py_If(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'elif' a=named_expression ':' b=block c=[else_block] { _Py_If(a, b, c, EXTRA) }
    | 'else''if' a=named_expression ':' b=block c=elif_stmt { _Py_If(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'else''if' a=named_expression ':' b=block c=[else_block] { _Py_If(a, b, c, EXTRA) }
else_block[asdl_stmt_seq*]: 'else' ':' b=block { b }

unless_stmt[stmt_ty]:
    | 'unless' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK((asdl_stmt_seq*)_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'unless' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) }
elun_stmt[stmt_ty]:
    | 'elun' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'elun' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) }
    | 'else''unless' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) }
    | 'else''unless' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) }
else2_block[asdl_stmt_seq*]: 'else' ':' b=block { b }

変更をしてmake regen-pegenmakeをすると、以下のようなエラーが出ました。

Parser/parser.c:4012:20: error: implicit declaration of function ‘_Py_Unless’; did you mean ‘_Py_alias’? [-Werror=implicit-function-declaration]
             _res = _Py_Unless ( a , b , CHECK ( ( asdl_stmt_seq * ) _PyPegen_singleton_seq ( p , c ) ) , EXTRA );
                    ^~~~~~~~~~
                    _Py_alias

implicit declaration of function ‘_Py_Unless’、つまり_Py_Unlessが宣言されていないようです。_Py_Ifがどこかに宣言されていないかと思い、grep _Py_If -r .で検索をかけてみると、Include/Python-ast.hにありました。ここで再び公式ドキュメントを参照すると、Parser/Python.asdlを変更してmake regen-astを実行することでInclude/Python-ast.hなどの関連ファイルが書き換えられるようです。以下のように変更してみます。

Parser/Python.asdl

          | If(expr test, stmt* body, stmt* orelse)
          | Unless(expr test, stmt* body, stmt* orelse)

make regen-astmakeをするとビルドに成功しました!

コンパイラの変更

ここまででソースコードをASTに変換することはできました。しかし意味を定義していないので、unless文のプログラムを実行しても何も表示されません。次はPython/compile.cを変更します。 ファイルを見てみると、compiler_if関数でif文が定義されているようです。compiler_ifで検索すると2ヶ所見つかるので、それを参考にcompiler_unlessを追加します。コメントを読んで、constantの真偽値は逆になるようにしました。

Python/compile.c

static int
compiler_unless(struct compiler *c, stmt_ty s)
{
    basicblock *end, *next;
    int constant;
    assert(s->kind == Unless_kind);
    end = compiler_new_block(c);
    if (end == NULL)
        return 0;

    constant = expr_constant(s->v.Unless.test);
    /* constant = 1: "unless 1", "unless 2", ...
     * constant = 0: "unless 0"
     * constant = -1: rest */
    if (constant == 1) { //真偽値を反転
        BEGIN_DO_NOT_EMIT_BYTECODE
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        END_DO_NOT_EMIT_BYTECODE
        if (s->v.Unless.orelse) {
            VISIT_SEQ(c, stmt, s->v.Unless.orelse);
        }
    } else if (constant == 0) {
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        if (s->v.Unless.orelse) {
            BEGIN_DO_NOT_EMIT_BYTECODE
            VISIT_SEQ(c, stmt, s->v.Unless.orelse);
            END_DO_NOT_EMIT_BYTECODE
        }
    } else {
        if (asdl_seq_LEN(s->v.Unless.orelse)) {
            next = compiler_new_block(c);
            if (next == NULL)
                return 0;
        }
        else {
            next = end;
        }
        if (!compiler_jump_if(c, s->v.Unless.test, next, 0)) {
            return 0;
        }
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        if (asdl_seq_LEN(s->v.Unless.orelse)) {
            ADDOP_JUMP(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;
}

(省略)

    case If_kind:
        return compiler_if(c, s);
    case Unless_kind:
        return compiler_unless(c, s);

makeしてpython3を実行してみると、unlessはif文と同じ挙動をしました。もう一度compiler_unless関数内をよく見ると、compiler_jump_if関数というものがあり、4つ目の引数がcondとなっていることがわかります。これは条件だとにらんで0を1に変えてみると…

Python/compile.c

static int
compiler_unless(struct compiler *c, stmt_ty s)
{
    basicblock *end, *next;
    int constant;
    assert(s->kind == Unless_kind);
    end = compiler_new_block(c);
    if (end == NULL)
        return 0;

    constant = expr_constant(s->v.Unless.test);
    /* constant = 1: "unless 1", "unless 2", ...
     * constant = 0: "unless 0"
     * constant = -1: rest */
    if (constant == 1) { //真偽値を反転
        BEGIN_DO_NOT_EMIT_BYTECODE
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        END_DO_NOT_EMIT_BYTECODE
        if (s->v.Unless.orelse) {
            VISIT_SEQ(c, stmt, s->v.Unless.orelse);
        }
    } else if (constant == 0) {
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        if (s->v.Unless.orelse) {
            BEGIN_DO_NOT_EMIT_BYTECODE
            VISIT_SEQ(c, stmt, s->v.Unless.orelse);
            END_DO_NOT_EMIT_BYTECODE
        }
    } else {
        if (asdl_seq_LEN(s->v.Unless.orelse)) {
            next = compiler_new_block(c);
            if (next == NULL)
                return 0;
        }
        else {
            next = end;
        }
        if (!compiler_jump_if(c, s->v.Unless.test, next, 1)) { //4つ目の引数condを0から1にする
            return 0;
        }
        VISIT_SEQ(c, stmt, s->v.Unless.body);
        if (asdl_seq_LEN(s->v.Unless.orelse)) {
            ADDOP_JUMP(c, JUMP_FORWARD, end);
            compiler_use![](https://i.imgur.com/0ZPZAAT.png)
_next_block(c, next);
            VISIT_SEQ(c, stmt, s->v.Unless.orelse);
        }
    }
    compiler_use_next_block(c, end);
    return 1;
}

(省略)

    case If_kind:
        return compiler_if(c, s);
    case Unless_kind:
        return compiler_unless(c, s);

できました!

まとめ・感想

Python構文解析のサポートがかなり手厚く、頑張れば文法を変更できるというのが体験できて楽しかったです。