はじめに
これは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
(変数)に対して++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
オペコードを定義します。
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==PreIncr
とop==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.c
のPython/compile.c
関数内の値の格納先に配列や構造体を追加することで実装できるのではと考えています。また、定数を対象としてしまった際に、専用のエラーメッセージが出るようにするなど、さらなる改良の余地があると思います。
おわりに
以上でインクリメントを実装することができました。CPythonという題材についてですが、公式のチェックリストがかなり充実しており、文法の変更手順がまとめられているので、変更を加えやすいかと思います。
また、CPythonのような大規模なソフトウェアに自ら手を加えてみて、全体的な概要を理解することが大切だと感じました。対象のソフトウェアについて、先程述べた公式のチェックリストのような資料を探し、そこから全体の流れをおおまかに理解することで、変更を加える必要のある箇所を絞り込んでいく地道な作業が実装の近道だと思います。
参考にしたサイト
- Python Developer’s Guide - 特にChanging CPython’s Grammarが文法を変更する際に役立ちました。
- Pythonに前置インクリメントを追加
- CPythonに後置インクリメントを加えてみた 実装編