PythonにC言語っぽい文法を追加する
※このブログはeeicの実験「大規模ソフトウェアを手探る」のレポートとして書かれたものです。
リンク
- 概要(Pythonを改造して機能を追加した話)
- PythonにC言語っぽい文法を追加する ←イマここ
・論理演算子として&&や||や!を追加する
・論理値のTrue,Falseをtrue,falseにも対応させる
・elifだけでなくelse ifにも対応させる - Pythonにオートインデント機能をつける
- (おまけ)Pythonにunless文を追加する
導入
Pythonにおいて論理式ではand
やor
,not
が用いられていて、C言語で使われているような&&
,||
,!
が使えません。この仕様はC言語ユーザーには間違いやすいと感じました(事実、筆者の一人は今でも間違えることがあります)。
そこで、C言語で使われている文法も許容するように改造しました。
公式のドキュメントを読む
CPythonを改造するに当たって、改造する方法がまとまった公式の開発者ガイドを見つけました。
文法を変えるときには
Grammar/python.gram
を変更してmake regen-pegen
コマンドを打つ新しい記号類を追加するには
Grammar/Tokens
を変更してmake regen-token
コマンドを打つ
ことがわかります。
このドキュメントはCPython3.9以降のバージョンを対象としています。CPython3.8以前ではGrammerフォルダの中身が異なり、違う変更が求められるため注意が必要です(実は最初はCPython3.8.6を改造していたのですが、これを理由に改造の対象をCPython3.10に変更しました)。
この変更はCPython3.9からパーサがLL(1)パーサからPEGパーサに変更されたことによるそうです (PEP 617 -- New PEG parser for CPython)
make regen-token
がうまくいかないとき
公式ドキュメントでは、Tokens
やpython.gram
を変更した際、make regen-token
,make regen-pegen
を実行することで自動で他の関連するファイルが書き換わると書かれています。しかし、このコマンドにはセイウチ演算子(:=
)が用いられているため、Python3.7以降を用いる必要があります(CPythonをビルドするためにPythonが必要です)。
作業を行った環境ではPython3.6がデフォルトで入っていたため、make regen-pegen
を実行しようとするとこの処理ができずにエラーを吐いてしまいました。
エラーの内容
Traceback (most recent call last): File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main "__main__", mod_spec) File "/usr/lib/python3.6/runpy.py", line 85, in _run_code exec(code, run_globals) File "/home/denjo/cpython/Tools/peg_generator/pegen/__main__.py", line 16, in <module> from pegen.build import Grammar, Parser, Tokenizer, ParserGenerator File "/home/denjo/cpython/Tools/peg_generator/pegen/build.py", line 10, in <module> from pegen.c_generator import CParserGenerator File "/home/denjo/cpython/Tools/peg_generator/pegen/c_generator.py", line 2, in <module> from dataclasses import field, dataclass ModuleNotFoundError: No module named 'dataclasses' Makefile:840: recipe for target 'regen-pegen' failed make: *** [regen-pegen] Error 1
ここではpyenvを用いてPythonのバージョンを3.8.0に変更します。
以下のサイトを参考にターミナル上でコマンドを叩いたところバージョンをPython3.8.0として指定することができました。
pyenvのインストール、使い方、pythonのバージョン切り替えできない時の対処法
$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile $ source ~/.bash_profile $ pyenv install 3.8.0 $ pyenv local 3.8.0
実際にバージョンが変更できたかはpython3 --version
で確認できます。
自分たちの環境では、ターミナルを起動するたびにsource ~/.bash_profile
を実行する必要がありました。
&&
を追加する
記号を追加する
実際にコードを書き換えていきます。ここでは、&&
をand
として解釈してもらうため&&
がCPythonの記号であると定義します。
Grammar/Tokens
RARROW '->' ELLIPSIS '...' COLONEQUAL ':='
↓ AND_ANDを追加する
RARROW '->' ELLIPSIS '...' COLONEQUAL ':=' AND_AND '&&'
ちなみに記号として登録すると前後の要素との間にスペースを必要としなくなります。例えばif AandB:
では認識されませんがif A&&B:
では認識されるというわけです。
文法を定義する
記号の追加が終わったら次は文法の定義を行います。&&
が実際にand
と同じ動作をするように定義します。Grammar/python.gram
内にand
の動作が記述してあるので、そこを参考にand
を&&
に変えたコードを追加します。
Grammar/python.gram
conjunction[expr_ty] (memo): | a=inversion b=('and' c=inversion { c })+ { _Py_BoolOp( And, CHECK(_PyPegen_seq_insert_in_front(p, a, b)), EXTRA) } | inversion
↓ &&
の文法を定義する
conjunction[expr_ty] (memo): | a=inversion b=('and' c=inversion { c })+ { _Py_BoolOp( And, CHECK(_PyPegen_seq_insert_in_front(p, a, b)), EXTRA) } | a=inversion b=('&&' c=inversion { c })+ { _Py_BoolOp( And, CHECK(_PyPegen_seq_insert_in_front(p, a, b)), EXTRA) } | inversion
あとはターミナル上でmake regen-token
とmake regen-pegen
を叩けば関連ファイルが書き換えられ、ビルドをすれば&&
がCPythonに認識されます!!
他の記号(||
と!
)の追加
同様にして||
と!
を追加していきます。
Tokens
に記号を追加して、python.gram
からor
,not
を定義している部分を見つけてそれを真似て追加するだけです。
(変更自体は&&
とほとんど同じことをするだけなので変更後だけ乗せます。)
Grammar/Tokens
OR_OR '||' NOT_ALIAS '!'
Grammar/python.gram
disjunction[expr_ty] (memo): | a=conjunction b=('or' c=conjunction { c })+ { _Py_BoolOp( Or, CHECK(_PyPegen_seq_insert_in_front(p, a, b)), EXTRA) } | a=conjunction b=('||' c=conjunction { c })+ { _Py_BoolOp( Or, CHECK(_PyPegen_seq_insert_in_front(p, a, b)), EXTRA) } | conjunction (省略) inversion[expr_ty] (memo): | '!' a=inversion { _Py_UnaryOp(Not, a, EXTRA) } | 'not' a=inversion { _Py_UnaryOp(Not, a, EXTRA) } | comparison
コード変更後も&&
と同じでmake regen-token
とmake regen-pegen
を叩くだけです。
True,Falseの小文字化
python.gram
からTrue
,False
を定義している部分を見つけてそれを真似て追加するだけです。
記号類の変更はないのでTokens
は変更する必要はありません。
Grammar/python.gram
atom[expr_ty]: | NAME | 'True' { _Py_Constant(Py_True, NULL, EXTRA) } | 'true' { _Py_Constant(Py_True, NULL, EXTRA) } | 'False' { _Py_Constant(Py_False, NULL, EXTRA) } | 'false' { _Py_Constant(Py_False, NULL, EXTRA) } | 'None' { _Py_Constant(Py_None, NULL, EXTRA) } | &STRING strings | NUMBER | &'(' (tuple | group | genexp) | &'[' (list | listcomp) | &'{' (dict | set | dictcomp | setcomp) | '...' { _Py_Constant(Py_Ellipsis, NULL, EXTRA) }
else ifも許容する
同様に変更します。
Grammar/python.gram
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 }
ここで追加したコードではelse if
の間の空白については判別を行っていないので、空白が複数あっても実行できるプログラムになっています。
実行結果
実際に以下の三点が実現できていることがわかると思います。
・論理演算子として&&や||や!を追加する
・論理値のTrue,Falseをtrue,falseにも対応させる
・elifだけでなくelse ifにも対応させる
まとめ・感想
CPythonはGrammarフォルダの内部で予約語や記号の定義が行われているので、既存の文法を参考にしたものを追加するだけならば簡単に実装できます。CPython3.9からパーサが変更されたおかげで先輩達が使っていたバージョンのときよりも簡単に文法をいじれるようになっている気がします。