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

# はじめに
これはeeic2022の3年後期実験「大規模ソフトウェアをて探ろう」の4班の成果報告の記事です。
ここではPythonにインクリメントを追加する方法について書きます。
以下は他の班員の記事です。CPythonのビルドやその他の機能の実装について書かれているので、参考にしてみてください。
- [Ubuntu20.04でCPythonをビルドしてみた](https://qiita.com/bnn877/items/8b197df99aea0c3cc56e)
- [CPythonに'&&', '||'を追加してみた](https://qiita.com/bnn877/items/3ad88383f0ff936e9822)
# 実装環境
- 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`(変数)に対して`++x`や`x++`などの表記ができるようになります。
`$ 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
オペコードを定義します。
~~~Python
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
以下のように`++`を追加します。
~~~Python
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`関数内
他の単項演算子に倣って追加しました。
~~~C
        /* Unary operators */
        case UNARY_POSITIVE:
        case UNARY_NEGATIVE:
        case UNARY_NOT:
        case UNARY_INVERT:
        case UNARY_PREINCREMENT:
        case UNARY_POSTINCREMENT:
            return 0;
~~~
### `unaryop`関数内
他の単項演算子に倣って追加しました。
~~~C
    case PreIncr:
        return UNARY_PREINCREMENT;
    case PostIncr:
        return UNARY_POSTINCREMENT;
~~~
### `compiler_visit_expr1`関数内
ここで実際のインクリメントの挙動を実装しています。前置インクリメントは+1された値を返すのに対し、後置インクリメントはそのままの値を返すという点に注意しながら、実装を進めていきます。
~~~C
    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==PreIncr`と`op==PostIncr`はそれぞれ前置インクリメントと後置インクリメントに対応しています。1.2行目の`VISIT`関数と`ADDOP`関数の部分はすべての単項演算子に共通しており、この部分で値を+1しています。
~~~C
VISIT(c, expr, e->v.UnaryOp.operand);
ADDOP(c, unaryop(e->v.UnaryOp.op));
~~~
さて、問題の返り値を+1するかどうかですが、その後の
~~~C
ADDOP_I(c, COPY, 1);
~~~
の行で+1した値を複製し返り値としています。したがって、後置インクリメントの場合はその行を削除するだけで実装できます。

さらに、以下の部分で値の格納を実装しています。
~~~C
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`においてその違いは実装できているので、ここでは同じ処理を行えばよいです。
~~~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.c`の`Python/compile.c`関数内の値の格納先に配列や構造体を追加することで実装できるのではと考えています。また、定数を対象としてしまった際に、専用のエラーメッセージが出るようにするなど、さらなる改良の余地があると思います。
# おわりに
以上でインクリメントを実装することができました。CPythonという題材についてですが、[公式のチェックリスト](https://devguide.python.org/developer-workflow/grammar/index.html)がかなり充実しており、文法の変更手順がまとめられているので、変更を加えやすいかと思います。
また、CPythonのような大規模なソフトウェアに自ら手を加えてみて、全体的な概要を理解することが大切だと感じました。対象のソフトウェアについて、先程述べた[公式のチェックリスト](https://devguide.python.org/developer-workflow/grammar/index.html)のような資料を探し、そこから全体の流れをおおまかに理解することで、変更を加える必要のある箇所を絞り込んでいく地道な作業が実装の近道だと思います。
# 参考にしたサイト
- [Python Developer’s Guide](https://devguide.python.org/) - 特に[Changing CPython’s Grammar](https://devguide.python.org/developer-workflow/grammar/index.html)が文法を変更する際に役立ちました。
- [Pythonに前置インクリメントを追加](https://kkti4216.hatenablog.com/entry/2020/10/30/025233)
- [CPythonに後置インクリメントを加えてみた　実装編](https://qiita.com/DOSS_INCREMENT/items/8b050847145f635211e2)