34
26

More than 1 year has passed since last update.

Pythonにインクリメントを追加してみた

Last updated at Posted at 2022-11-08

はじめに

これはeeic2022の3年後期実験「大規模ソフトウェアをて探ろう」の4班の成果報告の記事です。
ここではPythonにインクリメントを追加する方法について書きます。
以下は他の班員の記事です。CPythonのビルドやその他の機能の実装について書かれているので、参考にしてみてください。

実装環境

  • Ubuntu 20.04 LTS
  • Python3.10.8

インクリメント++の実装

CPython内における他の単項演算子-(-1倍)~(ビット反転)の定義を参考にしながら実装しました。
以下は実装した際の変更点です。

Grammer/Tokens

他の単項演算子と同様に以下を追加し、$ make regen-tokenを実行することで++をToken(意味を持った記号)として定義します。

INCREMENT            '++'

Grammer/python.gram

このファイルではPythonの文法が規定されています。
++という演算子がどのような文法で用いられるのかを記述しました。

factor[expr_ty] (memo):
    | '+' a=factor { _PyAST_UnaryOp(UAdd, a, EXTRA) }
    | '-' a=factor { _PyAST_UnaryOp(USub, a, EXTRA) }
    | '~' a=factor { _PyAST_UnaryOp(Invert, a, EXTRA) }
    | '++' a=factor { _PyAST_UnaryOp(PreIncr, a, EXTRA) } //前置インクリメント
    | a=factor '++' { _PyAST_UnaryOp(PostIncr, a, EXTRA) } //後置インクリメント
    | power

これにより、factor(変数)に対して++xx++などの表記ができるようになります。
$ make regen-pegenを実行する必要があります。

Parser/Python.asdl

抽象構文木(AST)を生成するためのファイルです。
python.gramに新たな定義を加えたので、こちらにも変更を加える必要があります。
以下のように、単項演算子が定義されている箇所にpython.gramで定義したPreIncr PostIncrを追加しました。

unaryop = Invert | Not | UAdd | USub | PreIncr | PostIncr

変更を加えた後、$ make regen-astを実行します。
何度もmakeコマンドを打つのが面倒な場合は$ make regen-allを実行することですべてmake regenしてくれます。

Lib/opcode.py

オペコードを定義します。

def_op('UNARY_PREINCREMENT', 13)
def_op('UNARY_POSTINCREMENT', 14)

Doc/library/dis.rst

++をオペコードに追加します。

.. opcode:: UNARY_PREINCREMENT

   Implements ``TOS = TOS + 1``.


.. opcode:: UNARY_POSTINCREMENT

   Implements ``TOS = TOS + 1``.

Lib/ast.py

以下のように++を追加します。

unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-", "PreIncr": "++", "PreIncr": "++"}
unop_precedence = {
    "not": _Precedence.NOT,
    "~": _Precedence.FACTOR,
    "+": _Precedence.FACTOR,
    "-": _Precedence.FACTOR,
    "++": _Precedence.FACTOR,
}

Python/compile.c

ここで抽象構文木(AST)からバイトコードに変換します。
3箇所に変更を加えます。

stack_effect関数内

他の単項演算子に倣って追加しました。

        /* Unary operators */
        case UNARY_POSITIVE:
        case UNARY_NEGATIVE:
        case UNARY_NOT:
        case UNARY_INVERT:
        case UNARY_PREINCREMENT:
        case UNARY_POSTINCREMENT:
            return 0;

unaryop関数内

他の単項演算子に倣って追加しました。

    case PreIncr:
        return UNARY_PREINCREMENT;
    case PostIncr:
        return UNARY_POSTINCREMENT;

compiler_visit_expr1関数内

ここで実際のインクリメントの挙動を実装しています。前置インクリメントは+1された値を返すのに対し、後置インクリメントはそのままの値を返すという点に注意しながら、実装を進めていきます。

    case UnaryOp_kind:
        if (e->v.UnaryOp.op==PreIncr){
            VISIT(c, expr, e->v.UnaryOp.operand);
            ADDOP(c, unaryop(e->v.UnaryOp.op));
            ADDOP_I(c, COPY, 1);
            assert( e->v.UnaryOp.operand->kind==Name_kind);
            compiler_nameop(c,e->v.UnaryOp.operand->v.Name.id,Store);
        } else if (e->v.UnaryOp.op==PostIncr) {
            VISIT(c, expr, e->v.UnaryOp.operand);
            ADDOP(c, unaryop(e->v.UnaryOp.op));
            //ADDOP_I(c, COPY, 1);
            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));
        }
        break;

上記のコードにおいて、op==PreIncrop==PostIncrはそれぞれ前置インクリメントと後置インクリメントに対応しています。1.2行目のVISIT関数とADDOP関数の部分はすべての単項演算子に共通しており、この部分で値を+1しています。

VISIT(c, expr, e->v.UnaryOp.operand);
ADDOP(c, unaryop(e->v.UnaryOp.op));

さて、問題の返り値を+1するかどうかですが、その後の

ADDOP_I(c, COPY, 1);

の行で+1した値を複製し返り値としています。したがって、後置インクリメントの場合はその行を削除するだけで実装できます。

さらに、以下の部分で値の格納を実装しています。

assert( e->v.UnaryOp.operand->kind==Name_kind);
compiler_nameop(c,e->v.UnaryOp.operand->v.Name.id,Store);

具体的にはassert関数でオペランド(被演算子)をName_kind(文字変数)に変更し、compiler_nameop関数で文字変数に結果を格納しています。

Python/ceval.c

バイトコードからどのように実行するかを記述します。既存の単項演算子-(-1倍)と~(ビット反転)の関数を用いることで+1が実装できます。前置/後置インクリメントの違いについてですが、compile.cにおいてその違いは実装できているので、ここでは同じ処理を行えばよいです。

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

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

実際の挙動を確認

以上の変更を加えた後、
$ make regen-all
$ make
$ make install
の順でコマンドを実行することでビルドできます。
ビルドしてできたディレクトリ内のbinディレクトリに移動し$ ./python3コマンドを実行することで、変更を加えた後のPythonが使用できます。
実際に追加したインクリメント++がどのような挙動をするか試してみると以下のようになりました。

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

前置インクリメントの場合は返り値が+1されていて、後置インクリメントの場合は+1されていないことがわかります。

今後の発展

今回の実装では変数に対して++x x++のように用いるインクリメントを実装しましたが、実際のインクリメントは配列や構造体に対しても用いることができます。
その部分の実装に関してはGrammer/python.gram内での定義や、Python/compile.cPython/compile.c関数内の値の格納先に配列や構造体を追加することで実装できるのではと考えています。また、定数を対象としてしまった際に、専用のエラーメッセージが出るようにするなど、さらなる改良の余地があると思います。

おわりに

以上でインクリメントを実装することができました。CPythonという題材についてですが、公式のチェックリストがかなり充実しており、文法の変更手順がまとめられているので、変更を加えやすいかと思います。
また、CPythonのような大規模なソフトウェアに自ら手を加えてみて、全体的な概要を理解することが大切だと感じました。対象のソフトウェアについて、先程述べた公式のチェックリストのような資料を探し、そこから全体の流れをおおまかに理解することで、変更を加える必要のある箇所を絞り込んでいく地道な作業が実装の近道だと思います。

参考にしたサイト

34
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
26