Pythonにunless文を追加する
※このブログはeeicの実験「大規模ソフトウェアを手探る」のレポートとして書かれたものです。
リンク
- 概要(Pythonを改造して機能を追加した話)
- PythonにC言語っぽい文法を追加する
・論理演算子として&&や||や!を追加する
・論理値のTrue,Falseをtrue,falseにも対応させる
・elifだけでなくelse ifにも対応させる - Pythonにオートインデント機能をつける
- (おまけ)Pythonにunless文を追加する ←イマここ
導入
実験をするにあたって、まずは先輩のunless文を追加してみたのレポートを参考にすることにしました。しかしPythonのバージョンの違いによりうまく行きませんでした。
実験終了後、途中まで手探ったunless
文を今なら実装できるのではと思いいじってみたらうまくいったのでおまけとして記録しておきたいと思います。
流れ
Pythonのコンパイルの流れは上記の公式ドキュメントに記されています。簡単に書くと以下の通りです。
ソースコード
↓ Parser/tokenizer.c
トークン列
↓ Parser/parser.c
AST(抽象構文木)
↓ Python/compile.c
CFG(制御フローグラフ)
↓ Python/compile.c
バイトコード(Pythonの仮想マシンが読み取るアセンブリ言語のようなもの)
ここで、ソースコードからASTへの変換をいじるには、Grammar/python.gram
やGrammar/Tokens
に変更を加え、make regen-token
やmake regen-pegen
コマンドで関連ファイルを書き換えます(PythonにC言語っぽい文法を追加するを参照)。また、ASTからバイトコードへの変換はPython/compile.c
に変更を加えればよさそうです。
パーサの変更
PythonにC言語っぽい文法を追加するでも書いたとおり、Grammar/python.gram
を変更してunless
を解析できるようにします。ここではif
の定義を参考にしたいので、ファイル内検索でif
文に関連した部分を探しました。
Grammar/python.gram
compound_stmt[stmt_ty]: | &('def' | '@' | ASYNC) function_def | &'if' if_stmt | &'unless' unless_stmt | &('class' | '@') class_def | &('with' | ASYNC) with_stmt | &('for' | ASYNC) for_stmt | &'try' try_stmt | &'while' while_stmt (省略) if_stmt[stmt_ty]: | 'if' a=named_expression ':' b=block c=elif_stmt { _Py_If(a, b, CHECK((asdl_stmt_seq*)_PyPegen_singleton_seq(p, c)), EXTRA) } | 'if' a=named_expression ':' b=block c=[else_block] { _Py_If(a, b, c, EXTRA) } 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 } unless_stmt[stmt_ty]: | 'unless' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK((asdl_stmt_seq*)_PyPegen_singleton_seq(p, c)), EXTRA) } | 'unless' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) } elun_stmt[stmt_ty]: | 'elun' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) } | 'elun' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) } | 'else''unless' a=named_expression ':' b=block c=elun_stmt { _Py_Unless(a, b, CHECK(_PyPegen_singleton_seq(p, c)), EXTRA) } | 'else''unless' a=named_expression ':' b=block c=[else2_block] { _Py_Unless(a, b, c, EXTRA) } else2_block[asdl_stmt_seq*]: 'else' ':' b=block { b }
変更をしてmake regen-pegen
とmake
をすると、以下のようなエラーが出ました。
Parser/parser.c:4012:20: error: implicit declaration of function ‘_Py_Unless’; did you mean ‘_Py_alias’? [-Werror=implicit-function-declaration] _res = _Py_Unless ( a , b , CHECK ( ( asdl_stmt_seq * ) _PyPegen_singleton_seq ( p , c ) ) , EXTRA ); ^~~~~~~~~~ _Py_alias
implicit declaration of function ‘_Py_Unless’
、つまり_Py_Unlessが宣言されていないようです。_Py_Ifがどこかに宣言されていないかと思い、grep _Py_If -r .
で検索をかけてみると、Include/Python-ast.h
にありました。ここで再び公式ドキュメントを参照すると、Parser/Python.asdl
を変更してmake regen-ast
を実行することでInclude/Python-ast.h
などの関連ファイルが書き換えられるようです。以下のように変更してみます。
Parser/Python.asdl
| If(expr test, stmt* body, stmt* orelse) | Unless(expr test, stmt* body, stmt* orelse)
make regen-ast
とmake
をするとビルドに成功しました!
コンパイラの変更
ここまででソースコードをASTに変換することはできました。しかし意味を定義していないので、unless
文のプログラムを実行しても何も表示されません。次はPython/compile.c
を変更します。
ファイルを見てみると、compiler_if関数でif文が定義されているようです。compiler_ifで検索すると2ヶ所見つかるので、それを参考にcompiler_unlessを追加します。コメントを読んで、constant
の真偽値は逆になるようにしました。
Python/compile.c
static int compiler_unless(struct compiler *c, stmt_ty s) { basicblock *end, *next; int constant; assert(s->kind == Unless_kind); end = compiler_new_block(c); if (end == NULL) return 0; constant = expr_constant(s->v.Unless.test); /* constant = 1: "unless 1", "unless 2", ... * constant = 0: "unless 0" * constant = -1: rest */ if (constant == 1) { //真偽値を反転 BEGIN_DO_NOT_EMIT_BYTECODE VISIT_SEQ(c, stmt, s->v.Unless.body); END_DO_NOT_EMIT_BYTECODE if (s->v.Unless.orelse) { VISIT_SEQ(c, stmt, s->v.Unless.orelse); } } else if (constant == 0) { VISIT_SEQ(c, stmt, s->v.Unless.body); if (s->v.Unless.orelse) { BEGIN_DO_NOT_EMIT_BYTECODE VISIT_SEQ(c, stmt, s->v.Unless.orelse); END_DO_NOT_EMIT_BYTECODE } } else { if (asdl_seq_LEN(s->v.Unless.orelse)) { next = compiler_new_block(c); if (next == NULL) return 0; } else { next = end; } if (!compiler_jump_if(c, s->v.Unless.test, next, 0)) { return 0; } VISIT_SEQ(c, stmt, s->v.Unless.body); if (asdl_seq_LEN(s->v.Unless.orelse)) { ADDOP_JUMP(c, JUMP_FORWARD, end); compiler_use_next_block(c, next); VISIT_SEQ(c, stmt, s->v.Unless.orelse); } } compiler_use_next_block(c, end); return 1; } (省略) case If_kind: return compiler_if(c, s); case Unless_kind: return compiler_unless(c, s);
make
してpython3を実行してみると、unless
はif文と同じ挙動をしました。もう一度compiler_unless関数内をよく見ると、compiler_jump_if関数というものがあり、4つ目の引数がcond
となっていることがわかります。これは条件だとにらんで0を1に変えてみると…
Python/compile.c
static int compiler_unless(struct compiler *c, stmt_ty s) { basicblock *end, *next; int constant; assert(s->kind == Unless_kind); end = compiler_new_block(c); if (end == NULL) return 0; constant = expr_constant(s->v.Unless.test); /* constant = 1: "unless 1", "unless 2", ... * constant = 0: "unless 0" * constant = -1: rest */ if (constant == 1) { //真偽値を反転 BEGIN_DO_NOT_EMIT_BYTECODE VISIT_SEQ(c, stmt, s->v.Unless.body); END_DO_NOT_EMIT_BYTECODE if (s->v.Unless.orelse) { VISIT_SEQ(c, stmt, s->v.Unless.orelse); } } else if (constant == 0) { VISIT_SEQ(c, stmt, s->v.Unless.body); if (s->v.Unless.orelse) { BEGIN_DO_NOT_EMIT_BYTECODE VISIT_SEQ(c, stmt, s->v.Unless.orelse); END_DO_NOT_EMIT_BYTECODE } } else { if (asdl_seq_LEN(s->v.Unless.orelse)) { next = compiler_new_block(c); if (next == NULL) return 0; } else { next = end; } if (!compiler_jump_if(c, s->v.Unless.test, next, 1)) { //4つ目の引数condを0から1にする return 0; } VISIT_SEQ(c, stmt, s->v.Unless.body); if (asdl_seq_LEN(s->v.Unless.orelse)) { ADDOP_JUMP(c, JUMP_FORWARD, end); compiler_use![](https://i.imgur.com/0ZPZAAT.png) _next_block(c, next); VISIT_SEQ(c, stmt, s->v.Unless.orelse); } } compiler_use_next_block(c, end); return 1; } (省略) case If_kind: return compiler_if(c, s); case Unless_kind: return compiler_unless(c, s);
できました!
まとめ・感想
Pythonにオートインデント機能をつける
※このブログはeeicの実験「大規模ソフトウェアを手探る」のレポートとして書かれたものです。
リンク
- 概要(Pythonを改造して機能を追加した話)
- PythonにC言語っぽい文法を追加する
・論理演算子として&&や||や!を追加する
・論理値のTrue,Falseをtrue,falseにも対応させる
・elifだけでなくelse ifにも対応させる - Pythonにオートインデント機能をつける ←イマここ
- (おまけ)Pythonにunless文を追加する
導入
ターミナルでPythonを使っていると、以下の画像のようにインデントを自分で入れる必要があります。 jupyter notebookでは自動でインデントを追加する機能があるのでターミナルでもこの機能を導入してあげたいと考えました。
入力を探す
オートインデント機能を実装するにあたって、まずユーザーからの入力を受け付ける部分を見つけます。具体的には、Emacs上でgdbを走らせて入力を受け付けていそうな関数の中にひたすらstep
で入っていきます。入力を求められたらブレークポイントを設定して最初から走らせるという流れを繰り返しているとPython/pythonrun.c
内のPyRun_InertactiveLoopFlags関数で、このようなコードを発見しました。
pythonrun.c/PyRun_InteractiveLoopFlags
do { ret = PyRun_InteractiveOneObjectEx(fp, filename, flags); if (ret == -1 && PyErr_Occurred()) { /* Prevent an endless loop after multiple consecutive MemoryErrors * while still allowing an interactive command to fail with a * MemoryError. */ if (PyErr_ExceptionMatches(PyExc_MemoryError)) { if (++nomem_count > 16) { PyErr_Clear(); err = -1; break; } } else { nomem_count = 0; } PyErr_Print(); flush_io(); } else { nomem_count = 0; } #ifdef Py_REF_DEBUG if (show_ref_count) { _PyDebug_PrintTotalRefs(); } #endif } while (ret != E_EOF);
このdo-while
ループによって入力、処理→エラーチェックを何度も繰り返しています。
この中で入力と処理を行っているのはPyRun_InteractiveOneObjectEx関数なので、この関数をまたstep
していくとModule/readline.c
の readline_until_enter_or_signal関数 といういかにも入力を受け付けていそうな関数に入ります。
readline.c/readline_until_enter_or_signal
static char * readline_until_enter_or_signal(const char *prompt, int *signal) { char * not_done_reading = ""; fd_set selectset; *signal = 0; #ifdef HAVE_RL_CATCH_SIGNAL rl_catch_signals = 0; #endif rl_callback_handler_install (prompt, rlhandler); FD_ZERO(&selectset); completed_input_string = not_done_reading; while (completed_input_string == not_done_reading) { int has_input = 0, err = 0; while (!has_input) { struct timeval timeout = {0, 100000}; /* 0.1 seconds */ /* [Bug #1552726] Only limit the pause if an input hook has been defined. */ struct timeval *timeoutp = NULL; if (PyOS_InputHook) timeoutp = &timeout; #ifdef HAVE_RL_RESIZE_TERMINAL /* Update readline's view of the window size after SIGWINCH */ if (sigwinch_received) { sigwinch_received = 0; rl_resize_terminal(); } #endif FD_SET(fileno(rl_instream), &selectset); /* select resets selectset if no input was available */ has_input = select(fileno(rl_instream) + 1, &selectset, NULL, NULL, timeoutp); err = errno; if(PyOS_InputHook) PyOS_InputHook(); } if (has_input > 0) { rl_callback_read_char(); } else if (err == EINTR) { int s; PyEval_RestoreThread(_PyOS_ReadlineTState); s = PyErr_CheckSignals(); PyEval_SaveThread(); if (s < 0) { rl_free_line_state(); #if defined(RL_READLINE_VERSION) && RL_READLINE_VERSION >= 0x0700 rl_callback_sigcleanup(); #endif rl_cleanup_after_signal(); rl_callback_handler_remove(); *signal = 1; completed_input_string = NULL; } } } return completed_input_string; }
上のコードのうちrl_callback_read_char関数 で入力を受け付けていること、rl_callback_handler_install関数 でターミナル上の>>>
や...
が入力されていることがわかりました。(正確には変数prompt
に>>>
や...
が格納されていて、rl_callback_handler_install関数で出力している。)
GNU Readlineについて
rl_callback_read_char関数を調べたところ、GNU Readlineと呼ばれるライブラリの関数であることがわかりました。rlはreadlineの略だそうです。
CPythonに入力させる
それでは、実際にインデントをプログラム側から入力するように変えていきます。
rl_callback_read_char関数の前に rl_insert_text("␣␣␣␣") (␣は空白)を追加すればインデントを挿入することができますが、これだけではキーボードから入力されるまでインデントが表示されませんでした。
そこで、ドキュメントを眺めていたところ、rl_line_bufferの現在の内容を画面上に反映させるrl_redisplay() 関数を見つけました。(これはバッファにたまった文字列を画面上に出力するC言語のfflush関数を想起させます。)
rl_insert_textの後にrl_redisplay()を入れると、即座にインデントが表示されるようになりました。
変数prompt
にはターミナル上で表示される>>>
や...
が格納されているので、とりあえず「prompt
が...
ならばインデントを行う」とすればブロック内ならインデントを1個だけ置くという処理が実装できます。
readline.c/readline_until_enter_or_signal
if(strcmp(prompt,"... ") == 0){ rl_insert_text(" "); } rl_redisplay();
直前の入力を読み込む
上記の実装だけではインデントが1個補完されるだけで、Pythonの入れ子構造に対応できていません。そのため、ユーザの直前の入力を持ってきてインデントの数を管理する必要があります。
Module/readline.c
について眺めていると直前の行の入力を受け取る部分がありました。直前の行の入力は変数line
に格納されています。
(以下のコード参照、直前と重複しない限り入力を履歴に追加するというコードのようです)
readline.c/call_readline
/* we have a valid line */ n = strlen(p); if (should_auto_add_history && n > 0) { const char *line; int length = _py_get_history_length(); if (length > 0) { HIST_ENTRY *hist_ent; if (using_libedit_emulation) { /* handle older 0-based or newer 1-based indexing */ hist_ent = history_get(length + libedit_history_start - 1); } else hist_ent = history_get(length); line = hist_ent ? hist_ent->line : ""; } else line = ""; if (strcmp(p, line)) add_history(p); }
このコードをコピーして不必要な部分を削れば直前の入力を文字列line
に格納するコードの出来上がりです。
readline.c/readline_until_enter_or_signal
const char *line; int length = _py_get_history_length(); HIST_ENTRY *hist_ent; if (using_libedit_emulation) { /* handle older 0-based or newer 1-based indexing */ hist_ent = history_get(length + libedit_history_start - 1); } else hist_ent = history_get(length); line = hist_ent ? hist_ent->line : "";
インデントの数を変更する
直前の入力line
を読み込んだので、先頭の文字からインデントを読みだしていきます。直前の入力のインデントと同じだけインデントを追加して、直前の入力に:
があったらさらにインデントを1つ追加します。また、コメントアウトされていた場合は、それ以上インデントの追加を行わないようにします。
readline.c/readline_until_enter_or_signal
if(strcmp(prompt,"... ") == 0){ const char *line; int length = _py_get_history_length(); HIST_ENTRY *hist_ent; if (using_libedit_emulation) { /* handle older 0-based or newer 1-based indexing */ hist_ent = history_get(length + libedit_history_start - 1); } else hist_ent = history_get(length); line = hist_ent ? hist_ent->line : ""; int len = strlen(line); for(int i = 0; i < len; i++){ if(line[i] == ' '){ rl_insert_text(" "); }else if(line[i] == '\t'){ rl_insert_text("\t"); }else{ break; } } for (int i=0;i<len;i++){ if(line[i] == '#'){ break; } else if(line[i]==':') { rl_insert_text(" "); break; } } } rl_redisplay();
これで望んだ動作を実現できました!
実装結果
最終的なコード
readline.c/readline_until_enter_or_signal
static char * readline_until_enter_or_signal(const char *prompt, int *signal) { char * not_done_reading = ""; fd_set selectset; *signal = 0; #ifdef HAVE_RL_CATCH_SIGNAL rl_catch_signals = 0; #endif rl_callback_handler_install (prompt, rlhandler); FD_ZERO(&selectset); completed_input_string = not_done_reading; if(strcmp(prompt,"... ") == 0){ const char *line; int length = _py_get_history_length(); HIST_ENTRY *hist_ent; if (using_libedit_emulation) { /* handle older 0-based or newer 1-based indexing */ hist_ent = history_get(length + libedit_history_start - 1); } else hist_ent = history_get(length); line = hist_ent ? hist_ent->line : ""; int len = strlen(line); for(int i = 0; i < len; i++){ if(line[i] == ' '){ rl_insert_text(" "); }else if(line[i] == '\t'){ rl_insert_text("\t"); }else{ break; } } for (int i=0;i<len;i++){ if(line[i] == '#'){ break; } else if(line[i]==':') { rl_insert_text(" "); break; } } } rl_redisplay(); while (completed_input_string == not_done_reading) { int has_input = 0, err = 0; while (!has_input) { struct timeval timeout = {0, 100000}; /* 0.1 seconds */ /* [Bug #1552726] Only limit the pause if an input hook has been defined. */ struct timeval *timeoutp = NULL; if (PyOS_InputHook) timeoutp = &timeout; #ifdef HAVE_RL_RESIZE_TERMINAL /* Update readline's view of the window size after SIGWINCH */ if (sigwinch_received) { sigwinch_received = 0; rl_resize_terminal(); } #endif FD_SET(fileno(rl_instream), &selectset); /* select resets selectset if no input was available */ has_input = select(fileno(rl_instream) + 1, &selectset, NULL, NULL, timeoutp); err = errno; if(PyOS_InputHook) PyOS_InputHook(); } if (has_input > 0) { rl_callback_read_char(); } else if (err == EINTR) { int s; PyEval_RestoreThread(_PyOS_ReadlineTState); s = PyErr_CheckSignals(); PyEval_SaveThread(); if (s < 0) { rl_free_line_state(); #if defined(RL_READLINE_VERSION) && RL_READLINE_VERSION >= 0x0700 rl_callback_sigcleanup(); #endif rl_cleanup_after_signal(); rl_callback_handler_remove(); *signal = 1; completed_input_string = NULL; } } } return completed_input_string; }
下のgif画像から、実際にインデントが必要な数だけ自動補完されていることがわかります。
まとめ・感想
- gdbを使ってプログラムの動作を一行ずつ見ながら特定の場所を探るということが「大規模ソフトウェアを手探」っている感があって面白く感じられました。
- 入出力に用いられるGNU readlineというモジュールの存在を知ることができたことはいい収穫でした。とくにrl_insert_text関数やrl_redisplay関数、history_get関数などを通してどのように入出力が行われているのか深く切り込むことができたのは非常にためになりました。
- CPythonという巨大なプログラムを手探る中で、readline_until_enter_or_signal関数というように関数名などが理解しやすく定義されていて、一人だけで扱うものでないプログラムにおいて後継を意識してコーディングすることの意義を感じられました。
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からパーサが変更されたおかげで先輩達が使っていたバージョンのときよりも簡単に文法をいじれるようになっている気がします。
Pythonを改造して機能を追加した話
※このブログはeeicの実験「大規模ソフトウェアを手探る」のレポートとして書かれたものです。
東京大学工学部電子情報工学科/電気電子工学科(eeic)の選択できる実験として「大規模ソフトウェアを手探る」があります。これは、OSS(オープンソースソフトウェア)として公開されている大規模なプログラムを改良/機能拡張することで、普段授業で扱うような小規模なプログラムでは触れられない、全容を把握することが困難なプログラムを扱う方法を身につけるというものです。 今回、自分たちは授業でも使い馴染みのあるPythonを手探ることにしました。
リンク
- 概要(Pythonを改造して機能を追加した話) ←イマここ
- PythonにC言語っぽい文法を追加する
・論理演算子として&&や||や!を追加する
・論理値のTrue,Falseをtrue,falseにも対応させる
・elifだけでなくelse ifにも対応させる - Pythonにオートインデント機能をつける
- (おまけ)Pythonにunless文を追加する
環境
- OS
Ubuntu20.04 / Ubuntu18.04 - 改造対象
Python3.10(cpython) - 使ったツール
Emacs, gdb
準備
まず作業用のディレクトリを作成します(ここでは~/cpython
)。
Github上のPython3.10(cpython)からgit clone
を用いてリポジトリを複製します。
$ mkdir cpython $ cd cpython $ git clone https://github.com/python/cpython
ビルド
configure
でMakefile
を作成します。このとき--prefix
オプションをもちいて最終的にプログラムをどこに配置するかを決定します(ここでは~/python-install
)。また環境変数CFLAGS
に-O0
を付けることで最適化レベルを落とし、さらに-g
を付けることで実行可能ファイルに「デバッグシンボル」を含めます。こうすることで、gdbを使ってプログラムの挙動を1行ずつ追うことができます。
configure
でMakefile
が作成できたら、make
とmake install
でコンパイルとインストールを行います。
$ CFLAGS="-g -O0" ./configure --prefix=/home/[username]/python-install/ $ make $ make install
これでビルドが完了し、~/python-install/bin/python3
を実行することで、pythonが起動できます。
コードを変更したときは、make clean
をしてからmake
をする必要があります。
デバッガで追跡
Emacsを起動し、M-x shell
コマンドで~/python-install/bin/
に移動します。
移動できたらM-x gud-gdb
とgdb --fullname python3
を叩いてgdbを起動、これを用いてプログラムの追跡を行いました。
参考資料
- 実験のホームページです。
CPythonの公式ドキュメント(Changing CPython’s Grammar)
CPythonの公式ドキュメント(Design of CPython’s Compiler)
- pythonに文法を追加する際にどのファイルを変更すればよいかが記載されています。
- Pythonにオートインデント機能をつけるで参考にしたドキュメントです。