Pythonに前置インクリメントを追加

はじめに

この記事は東京大学工学部電子情報工学科3年後期実験「大規模ソフトウェアを手探る」の報告記事です。

ここではPython3.10に前置インクリメントを追加する方法について書きます。

以下は同じ班だった同期の記事です。先に「ビルド&構造把握」の記事を読むことをお勧めします。

環境

version
Ubuntu 18.04
grep 3.1
gdb 8.1.0
vscode 1.49.1

python自体のビルドについては班員の記事に書いてあります。

実装

基本的には他の単項演算子である "-" (-1倍)、 "~" (ビット反転)を参考に進めていきます。

これらの単項演算子との違いは変数の値が更新されるところにあります。

公式サイト24. Changing CPython’s Grammarにしたがって書き換えていくとほとんどのファイルは自動生成されます。

Grammar/Tokens(L56)

"++"をトークンとして定義します

INCREMENT               '++'

Grammar/python.gram(L458)

"++"が単項演算子であることを定義します。単項演算子の部分に付け加えましょう。

factor[expr_ty] (memo):
    | '+' a=factor { _Py_UnaryOp(UAdd, a, EXTRA) }
    | '-' a=factor { _Py_UnaryOp(USub, a, EXTRA) }
    | '~' a=factor { _Py_UnaryOp(Invert, a, EXTRA)}
    | '++' a=NAME { _Py_UnaryOp(UInc, a, EXTRA)}
    | power

factorではなくNAMEとなっているのは"++"が付くのは変数に対してのみだからです。++(-x)だと-xをインクリメントすることになり文法として望ましくないです。一方 ~(-x)は値の更新がないので問題ありません。

Parser/Python.asdl(L99)

こちらも他の単項演算子に倣って付け加えます。

unaryop = Invert | Not | UAdd | USub | UInc

Python/ast_unparse.c(L179)

ここでは演算子の優先度を定義しています。優先度が本当にこれで適切かは考慮するべきかもしれません。

switch (e->v.UnaryOp.op) {
    case Invert: op = "~"; pr = PR_FACTOR; break;
    case Not: op = "not "; pr = PR_NOT; break;
    case UAdd: op = "+"; pr = PR_FACTOR; break;
    case USub: op = "-"; pr = PR_FACTOR; break;
    case UInc: op = "++"; pr = PR_FACTOR; break;
    default:
        ...

Lib/opcode.py(L68)

インクリメントのオペコードを定義しています。

def_op('UNARY_POSITIVE', 10)
def_op('UNARY_NEGATIVE', 11)
def_op('UNARY_NOT', 12)
def_op('UNARY_INCREMENT', 13)

Python/compile.c(L5010)

ここまでで定義してきた単項演算子の実際の動作を書いていきます。バイトコードで処理を考えていきましょう。 今回の実装ではインクリメントのオペコードは「+1した値を返す」を行い、演算子の処理で値の更新を行いました。

case UnaryOp_kind:
    if(e->v.UnaryOp.op==UInc){
      VISIT(c, expr, e->v.UnaryOp.operand);
      ADDOP(c, unaryop(e->v.UnaryOp.op));
      ADDOP(c, DUP_TOP);
      assert( e->v.UnaryOp.operand->kind==Name_kind);
      compiler_nameop(c,e->v.UnaryOp.operand->v.Name.id,Store);
    }else{
       VISIT(c, expr, e->v.UnaryOp.operand);
       ADDOP(c, unaryop(e->v.UnaryOp.op));
    }

else以下が既存の単行演算子の処理です。 if以下が"++"の処理ですが、最初の2文は他の単行演算子と同様です。スタックの一番上に+1された後の値が積まれているのでそれをADDOPで複製、一番上をプッシュして値を更新します。

複製を行わないとインクリメントそした時の返り値がなくなってしまいます。

Python/ceval.c(L1608)

インクリメントのオペコードを記述します。スタックの頭をプッシュして+1して積めば良いです。

case TARGET(UNARY_INCREMENT): {
        PyObject *right = TOP();
        PyObject *inv,*sum;
        //-(~x)=x+1
            inv=PyNumber_Invert(right);
            if (inv == NULL)
          goto error;
            sum = PyNumber_Negative(inv);
            Py_DECREF(inv);
            if (sum == NULL)
              goto error;
        Py_DECREF(right);
            SET_TOP(sum);
        DISPATCH();
        }

既存のオペコードとしてビット反転と-1倍がありますのでそれを利用すれば以下のように+1という演算は実装できます。

    x + ~x = 11...11 = 00...00 - 1 = -1 

<=>  x + 1 = -(~x)

確認

以下のようにすればインクリメント演算子が追加されたPythonが使用できるようになります。

make clean
make regen-all
make
make install

ちゃんと動きます!

>>> a=0
>>> ++a
1
>>> a
1

まとめ

ファイルの自動生成が優秀で公式のチェックリストに従って変更していけばそこそこ進められると思います。自動生成されるファイルも大体ファイル冒頭にどのファイルを元に生成されているのかなど詳しく書いてくれているのでとても親切でした。何も考えずに走らせてたPythonが内部でしていることに触れられてとても勉強になりました。

参考