pythonを改変してdowhile文を使えるようにしてみた

はじめに

この記事は, 東京大学工学部電子情報工学科・電気電子工学科(通称EEIC)の3年後期実験の1つである「大規模ソフトウェアを手探る」のレポートとして書かれたものです.

「大規模ソフトウェアを手探る」という実験では, 2, 3人のチームを組み, 大規模なオープンソースソフトウェア(OSS)を1つ以上選んで, 機能を拡張したり, バグを修正するという実験です. 私達の班「cuatro」では, pythonを改変するOSSとして選定し, 実際に機能拡張や改変を行ってみました.

概要

私達の班「cuatro」では, 以下の3点の改変を行いました.

  • 全角スペースを含んだときに出るエラーメッセージを改変する.
  • do-while文を実装する.
  • classのメソッドの引数の省略を試みる.

本記事では2つ目の「do-while文を実装する」というものの説明です.

なお, 1つ目と3つ目については, 班のメンバーが以下の記事を書いています.興味のある方は合わせてご覧ください.

1つ目「エラーメッセージの変更」

https://massiilo.hatenablog.com/entry/2021/11/07/064957

3つ目「クラスにおいて、 selfを省略可能にする」

https://rachmanix.hatenablog.com/entry/2021/11/05/183056

なお, 改変したコードについては, 以下の本実験のgithubリポジトリにあります.

https://doss-gitlab.eidos.ic.i.u-tokyo.ac.jp/cuatro/python

本記事の「do-while文を実装する」については, ディレクトhkatori/cpythonにあります.

環境

インストール

cpythonを以下のgithubリポジトリよりcloneしました. ターミナルからgitコマンドを用いて以下のようにします.

$ git clone https://github.com/python/cpython.git

すると, カレントディレクトリに名称が衝突しなければ, cpythonというディレクトリができるはずです. 以下は, 使用したgithubリポジトリのリンクです.

https://github.com/python/cpython

do-whileとは

通常のwhile文では条件判定を最初に行いますが, do-while文では, 条件判定を処理が終わった後の最後に行います. ですので, 以下のpythonコードではwhile文では, 一度も処理がなされないのに対し, do-while文では, 一回だけ処理がなされます.

a = 5
while(a > 5): print(a)   #処理は一度も実行されない
dowhile(a > 5): print(a) #処理は一度だけ実行される

このようにdo-while文では, 少なくとも一回は処理を行うことが特徴としてあげられ, 例えば, パスワードの入力を求めるシステム(入力してもらって一致判定を行い, 必要であれば, 一致しない場合はもう一度入力をユーザに求める)の処理は正しくdo-while文そのものです.

なお, do-whileはwhile文を用いて以下のように書けます. また, pythonにはdo-while文は実装されていません. c言語c++にはあります.

a = 5
while True:
  print(a)
  if (a > 5): break

cpythonとは

c言語で記述されているpythonのリファレンス実装のことです. 作者によって作られたバージョンで, 通常のpythonはこれを指すことが多いです.

手探る

ビルド

まず, cloneしたcpythonが正しく動作するか確認します. 改変するpythonが入っているディレクトcpythonに遷移します.

$ cd cpython

コンパイルに関するいろいろな設定や, 環境の情報の入手, インストール場所の決定を行うのがconfigureです. なお, オプションとして, -O0, -gを指定していますが, デバッガで追跡するためにつけると便利なもので, 前者は最適化レベルを最低に落とし, 後者は実行可能ファイルにデバッグシンボル(コンパイル後の命令列とソースコード上の位置の対応関係)をつけるオプションです. またprefixオプションでインストール先のフォルダを指定しています.

$ CFLAGS="-O0 -g" ./configure --prefix=/home/denjo/cpython_install

次にコンパイルとインストールを行います.

$ make
$ make install

これでビルドは完了です. 実行して正しく動作することが確認できました.

大まかな流れ

Python Developer's Guideという公式のドキュメントや過去にpythonを手探った先輩たちのブログを参考に, どのようなことが必要で, どのような処理をしているのかを把握しました. ざっくりとまとめると以下のような感じになります.

Python Developer's Guide

f:id:aiutarsi:20211029211054p:plain

ここで, 上の図のディレクトリ名/ファイル名は改変の必要な可能性のあるファイルです. 今回の改変では上の図の青色から黄色の部分までが変更対象で, バイトーコードから機械語(CPUが理解できる言語)までの変更は不要です.

f:id:aiutarsi:20211029211319p:plain

  • CFG(Control Flow Graph) : プログラムを実行したときに, 通る可能性のある経路をグラフ化したもの.

  • バイトコード : テキスト・バイナリなどの中間表現(ソースコード機械語の 間のもの)

改変

Grammar/Python.gram

このファイルは簡単に言うとpythonの文法を定義しているものです. 今回は新しくdo-while文を追加したいので, do-whileの定義を追加することになります. しかし, do-whileは処理手順が違うことがwhileとの違いなので, より具体的には, whileでは条件判定を最初に行いますが, do-whileでは条件判定を最後に行います. ですので, 定義自体はwhileの真似をすれば大丈夫で, Python.gramではwhileを真似してdo-whileのバージョンを作れば良いだけです.

grepコマンドでwhile関連の定義がなされている箇所をPython.gramから探します.

ここでは, ファイル中にwhile_stmtという文字列が入っている箇所を検索します. オプションとしては以下を指定しています.

  • -r : ディレクトリ以下を探索します(今回の場合はカレントディレクトリがGrammarなので, Grammar以下のファイルを探索します).

  • -n : 行数を表示します.

  • -I : バイナリファイルをマッチするデータを含まないかのように処理します.

$ grep -r -n -I "while_stmt"
python.gram:135:    | &'while' while_stmt
python.gram:364:while_stmt[stmt_ty]:
python.gram:365:    | invalid_while_stmt
python.gram:1229:invalid_while_stmt:

どうやら追加すべき箇所は135行目付近, 364行目付近, 1229行目付近の3箇所のようです. それぞれの箇所を見て, 空気を読んでdo-whileを追加していきます.

  • 135行目付近.

Before

compound_stmt[stmt_ty]:
    | &('def' | '@' | ASYNC) function_def
    | &'if' if_stmt
    | &('class' | '@') class_def
    | &('with' | ASYNC) with_stmt
    | &('for' | ASYNC) for_stmt
    | &'try' try_stmt
    | &'while' while_stmt
    | match_stmt

After

compound_stmt[stmt_ty]:
    | &('def' | '@' | ASYNC) function_def
    | &'if' if_stmt
    | &('class' | '@') class_def
    | &('with' | ASYNC) with_stmt
    | &('for' | ASYNC) for_stmt
    | &'try' try_stmt
    | &'while' while_stmt
    | &'dowhile' dowhile_stmt
    | match_stmt
  • 364行目付近

Before

# While statement
# ---------------

while_stmt[stmt_ty]:
    | invalid_while_stmt
    | 'while' a=named_expression ':' b=block c=[else_block] { _PyAST_While(a, b, c, EXTRA) }

# For statement
# -------------

After

# While statement
# ---------------

while_stmt[stmt_ty]:
    | invalid_while_stmt
    | 'while' a=named_expression ':' b=block c=[else_block] { _PyAST_While(a, b, c, EXTRA) }


# DoWhile statement
#------------------

dowhile_stmt[stmt_ty]:
    | invalid_dowhile_stmt
    | 'dowhile' a=named_expression ':' b=block c=[else_block] { _PyAST_DoWhile(a, b, c, EXTRA) }

# For statement
# -------------
  • 1229行目付近

Before

invalid_while_stmt:
    | 'while' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
    | a='while' named_expression ':' NEWLINE !INDENT {
        RAISE_INDENTATION_ERROR("expected an indented block after 'while' statement on line %d", a->lineno) }
invalid_for_stmt:

After

invalid_while_stmt:
    | 'while' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
    | a='while' named_expression ':' NEWLINE !INDENT {
        RAISE_INDENTATION_ERROR("expected an indented block after 'while' statement on line %d", a->lineno) }
invalid_dowhile_stmt:
    | 'dowhile' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
    | a='dowhile' named_expression ':' NEWLINE !INDENT {
        RAISE_INDENTATION_ERROR("expected and indented block after 'dowhile' statement on line %d", a->lineno) }
invalid_for_stmt:

さらに, Python Developer's GuideによるとこのGrammar/Python.gramと言うファイルを改変したら以下のコマンドを実行してねとの記述があったのでその通りにします. 公式リファレンスによると, どうやらこのコマンドを実行することでパーサ(parser.c)を自動的に再生成しているようです.

$ make regen-pegen

しかし, 残念なことに下記のようなエラーを吐かれました.

PYTHONPATH=./Tools/peg_generator python3 -m pegen -q c \
        ./Grammar/python.gram \
        ./Grammar/Tokens \
        -o ./Parser/parser.new.c
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/python/hkatori/cpython/Tools/peg_generator/pegen/__main__.py", line 15, in <module>
    from pegen.build import Grammar, Parser, ParserGenerator, Tokenizer
  File "/home/denjo/python/hkatori/cpython/Tools/peg_generator/pegen/build.py", line 9, in <module>
    from pegen.c_generator import CParserGenerator
  File "/home/denjo/python/hkatori/cpython/Tools/peg_generator/pegen/c_generator.py", line 3, in <module>
    from dataclasses import dataclass, field
ModuleNotFoundError: No module named 'dataclasses'
Makefile:966: recipe for target 'regen-pegen' failed
make: *** [regen-pegen] Error 1

これについては, 過去にpythonを手探った先輩の中に同じ現象に遭遇した方がいたので, その人の対処法を参考にしました(いや, ありがたい). https://qiita.com/koooooo/items/b21d87ffe2b56d0c589b

どうやら, デフォルトで自分の環境にインストールされているpythonのバージョンが3.6.9なのですが, それだとこのmake regen-pegenコマンドは動かない部分があるので, 新しいバージョンのpythonで実行する必要があるようです. ということで以下のサイトの手順に従い, 新しいバージョンのpythonをインストールしました. https://www.python.jp/install/ubuntu/index.html

$ sudo apt install
$ sudo apt install build-essential libbz2-dev libdb-dev \
  libreadline-dev libffi-dev libgdbm-dev liblzma-dev \
  libncursesw5-dev libsqlite3-dev libssl-dev \
  zlib1g-dev uuid-dev tk-dev

より最新バージョンである3.10.0をダウンロードし、解答してディレクトPython-3.10.0ができるのでそこに入って

$ ./configure
$ make
$ make install
$ python3 -V
Python 3.10.0

よりちゃんとインストールできていることが確認されました. そして, もう一回regen-pegenします.

$ make regen-pegen
PYTHONPATH=./Tools/peg_generator python3 -m pegen -q c \
        ./Grammar/python.gram \
        ./Grammar/Tokens \
        -o ./Parser/parser.new.c
python3 ./Tools/scripts/update_file.py ./Parser/parser.c ./Parser/parser.new.c

エラーは出ませんでした. 上手くいったようです.

Grammar/Tokens

中身を見ると>=}などといった演算子などのトークンを定義しているようなので, 新しい関数を追加することに対して, 新しいタイプのトークンは生成していないので, 変更の必要はないです. 一応regen-tokenしておきます. やっても益はあっても害はないはず.

$ make regen-token
# Regenerate Doc/library/token-list.inc from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3 ./Tools/scripts/generate_token.py rst \
        ./Grammar/Tokens \
        ./Doc/library/token-list.inc
# Regenerate Include/token.h from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3 ./Tools/scripts/generate_token.py h \
        ./Grammar/Tokens \
        ./Include/token.h
# Regenerate Parser/token.c from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3 ./Tools/scripts/generate_token.py c \
        ./Grammar/Tokens \
        ./Parser/token.c
# Regenerate Lib/token.py from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3 ./Tools/scripts/generate_token.py py \
        ./Grammar/Tokens \
        ./Lib/token.py

Parser/Python.asdl

ASTを構成する過程のもので, ブロックの形での記述をしています. こちらもgrepで該当箇所を探して, whileの真似をしてdo-whileのバージョンを作るだけです.

$ grep -n While "Python.asdl"
34:          | While(expr test, stmt* body, stmt* orelse)

34行目に追記します.

Before

          | While(expr test, stmt* body, stmt* orelse)
          | If(expr test, stmt* body, stmt* orelse)

After

          | While(expr test, stmt* body, stmt* orelse)
          | DoWhile(expr test, stmt* body, stmt* orelse)
          | If(expr test, stmt* body, stmt* orelse)

orelseはつけることにしました. orelseとは、while文の条件式が偽になったときにelse句の内容が実行されます. これは, 手探って初めて知った仕様でした. これも大規模なソフトウェアを手探ることで得られる副産物です.

以下はorelseの説明です. orelseはforやifでも定義されています. 確かにelseは少なくともifがないと呼ばれないので, ifでorelseが定義されているのも納得ですし, むしろelseはそれ単体としてではなく, ifやforなどと共に定義されて然るべきだと思いました.

while 条件:
    実行1
else:
    実行2

無論, なくても処理は書けますが, あった方が便利な例として, もしwhile文の処理にbreakを仕込んで、実際にbreakしたときにはelseが実行されないようなことが考えられます.

公式ドキュメント通りregen-astコマンドを実行します.

$ make regen-ast
# Regenerate 3 files using using Parser/asdl_c.py:
# - Include/internal/pycore_ast.h
# - Include/internal/pycore_ast_state.h
# - Python/Python-ast.c
/bin/mkdir -p ./Include
/bin/mkdir -p ./Python
python3 ./Parser/asdl_c.py \
        ./Parser/Python.asdl \
        -H ./Include/internal/pycore_ast.h.new \
        -I ./Include/internal/pycore_ast_state.h.new \
        -C ./Python/Python-ast.c.new
Python/Python-ast.c.new, Include/internal/pycore_ast.h.new, Include/internal/pycore_ast_state.h.new regenerated.
python3 ./Tools/scripts/update_file.py ./Include/internal/pycore_ast.h ./Include/internal/pycore_ast.h.new
python3 ./Tools/scripts/update_file.py ./Include/internal/pycore_ast_state.h ./Include/internal/pycore_ast_state.h.new
python3 ./Tools/scripts/update_file.py ./Python/Python-ast.c ./Python/Python-ast.c.new

Python/ast.c

こちらもgrepで該当箇所を探して変更します.

Before

    case While_kind:
        ret = validate_expr(state, stmt->v.While.test, Load) &&
            validate_body(state, stmt->v.While.body, "While") &&
            validate_stmts(state, stmt->v.While.orelse);
        break;
    case If_kind:

After

    case While_kind:
        ret = validate_expr(state, stmt->v.While.test, Load) &&
            validate_body(state, stmt->v.While.body, "While") &&
            validate_stmts(state, stmt->v.While.orelse);
        break;
    case DoWhile_kind:
        ret = validate_expr(state, stmt->v.DoWhile.test, Load) &&
            validate_body(state, stmt->v.DoWhile.body, "DoWhile") &&
            validate_stmts(state, stmt->v.DoWhile.orelse);
        break;
    case If_kind:

Parser/tokenizer.c

grepしてみたところ, 変更が必要な箇所はないようです.

Parser/parser.c

Grammar/Python.gramでも述べた通り, regen-pegenコマンドで自動再生成したので変更不要です.

Python/symtable.c

構文解析パートのファイルの変更は終わったので, この項からはAST->CFG->バイトコードにおけるファイルの変更です.

symtable.cは, ASTからシンボルの表を作成するファイルのようです. grepで変更箇所を特定します.

$ grep -n While_kind symtable.c
1310:    case While_kind:

Before

    case While_kind:
        VISIT(st, expr, s->v.While.test);
        VISIT_SEQ(st, stmt, s->v.While.body);
        if (s->v.While.orelse)
            VISIT_SEQ(st, stmt, s->v.While.orelse);
        break;
    case If_kind:

After

    case While_kind:
        VISIT(st, expr, s->v.While.test);
        VISIT_SEQ(st, stmt, s->v.While.body);
        if (s->v.While.orelse)
            VISIT_SEQ(st, stmt, s->v.While.orelse);
        break;
    case DoWhile_kind:
        VISIT(st, expr, s->v.DoWhile.test);
        VISIT_SEQ(st, stmt, s->v.DoWhile.body);
        if (s->v.DoWhile.orelse)
            VISIT_SEQ(st, stmt, s->v.DoWhile.orelse);
        break;
    case If_kind:

Python/compile.c

今までは定義を新たに追加するようなことばかりしてきましたが, これが本丸です. このファイルでは, ファイル名が示唆する通り, ASTからバイトコードを生成するファイルです.

しかし, やることはまず, grepでwhile関連が書かれているところを探してdo-whileのバージョンを追記するという今までのやり方は変わりません.

$ grep -n while compile.c
240:The u pointer points to the current compilation unit, while units
385:    while (PyUnicode_READ_CHAR(privateobj, ipriv) == '_')
614:    while (b != NULL) {
1424:        while (_PySet_NextEntry(o, &pos, &item, &hash)) {
3025:compiler_while(struct compiler *c, stmt_ty s)
3402:        while (1) {
3628:        return compiler_while(c, s);
4704:  The LC/SC version returns the populated container, while the GE version is
5858:    while (pc->fail_pop_size < size) {
5888:    while (--pc->fail_pop_size) {
6322:            while (icontrol--) {
6355:                    while (rotations--) {
6693:    while (sp != stack) {
6906:    while (todo > todo_stack) {
7094:        while (ldelta > 127) {
7100:        while (ldelta < -127) {
7108:    while (bdelta > 254) {
7296:    } while (extended_arg_recompile);
7308:    while (PyDict_Next(dict, &pos, &k, &v)) {
7327:    while (PyDict_Next(dict, &pos, &k, &v)) {
7417:    while (PyDict_Next(c->u->u_varnames, &pos, &k, &v)) {
7433:    while (PyDict_Next(c->u->u_cellvars, &pos, &k, &v)) {
7447:    while (PyDict_Next(c->u->u_freevars, &pos, &k, &v)) {
7625:    while (PyDict_Next(c->u->u_cellvars, &pos, &varname, &cellindex)) {
8136:            while (inst->i_target->b_iused == 0) {
8428:                while (next && next->b_iused == 0) {
8477:                while (bb->b_instr[i].i_target->b_iused == 0) {
8495:    while (sp > stack) {
8524:            while (next->b_iused == 0 && next->b_next) {
8536:            while (target->b_iused == 0) {
8735:        while (b->b_next && b->b_next->b_iused == 0) {
$ grep -n While compile.c
1758:        case While_kind:
1759:            res = find_ann(st->v.While.body) ||
1760:                  find_ann(st->v.While.orelse);
3039:    if (!compiler_jump_if(c, s->v.While.test, anchor, 0)) {
3044:    VISIT_SEQ(c, stmt, s->v.While.body);
3046:    if (!compiler_jump_if(c, s->v.While.test, body, 1)) {
3053:    if (s->v.While.orelse) {
3054:        VISIT_SEQ(c, stmt, s->v.While.orelse);
3627:    case While_kind:

どうやら変更箇所は, 1758行目, 3025行目, 3627行目付近の3箇所のようです.

  • 1758行目付近.

Before

        case While_kind:
            res = find_ann(st->v.While.body) ||
                  find_ann(st->v.While.orelse);
            break;
        case If_kind:

After

        case While_kind:
            res = find_ann(st->v.While.body) ||
                  find_ann(st->v.While.orelse);
            break;
        case DoWhile_kind:
            res = find_ann(st->v.DoWhile.body) ||
                  find_ann(st->v.DoWhile.orelse);
            break;
        case If_kind:
  • 3628行目付近.

Before

    case While_kind:
        return compiler_while(c, s);
    case If_kind:

After

    case While_kind:
        return compiler_while(c, s);
    case DoWhile_kind:
        return compiler_dowhile(c, s);
    case If_kind:
  • 3025行目付近.

compile.cWhile test : body:のようにブロックごとに分割したものを訪ねる順序を記し, バイトコードに変更する役割を担っています. つまり, while文とdo-while文の違いは条件分岐を前置するか後置するかの違いだったので, 以下のように, jump_if関数とVISIT_SEQ関数の順序を入れ替えることで, body(処理)を先に, test(条件分岐)を後にできます. これによりdo-while文は実現できます.

Before

static int
compiler_while(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *body, *end, *anchor = NULL;
    loop = compiler_new_block(c);
    body = compiler_new_block(c);
    anchor = compiler_new_block(c);
    end = compiler_new_block(c);
    if (loop == NULL || body == NULL || anchor == NULL || end == NULL) {
        return 0;
    }
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, WHILE_LOOP, loop, end, NULL)) {
        return 0;
    }
    if (!compiler_jump_if(c, s->v.While.test, anchor, 0)) {
        return 0;
    }

    compiler_use_next_block(c, body);
    VISIT_SEQ(c, stmt, s->v.While.body);
    SET_LOC(c, s);
    if (!compiler_jump_if(c, s->v.While.test, body, 1)) {
        return 0;
    }

    compiler_pop_fblock(c, WHILE_LOOP, loop);

    compiler_use_next_block(c, anchor);
    if (s->v.While.orelse) {
        VISIT_SEQ(c, stmt, s->v.While.orelse);
    }
    compiler_use_next_block(c, end);

    return 1;
}

static int
compiler_return(struct compiler *c, stmt_ty s)
{

After

static int
compiler_while(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *body, *end, *anchor = NULL;
    loop = compiler_new_block(c);
    body = compiler_new_block(c);
    anchor = compiler_new_block(c);
    end = compiler_new_block(c);
    if (loop == NULL || body == NULL || anchor == NULL || end == NULL) {
        return 0;
    }
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, WHILE_LOOP, loop, end, NULL)) {
        return 0;
    }
    if (!compiler_jump_if(c, s->v.While.test, anchor, 0)) {
        return 0;
    }

    compiler_use_next_block(c, body);
    VISIT_SEQ(c, stmt, s->v.While.body);
    SET_LOC(c, s);
    if (!compiler_jump_if(c, s->v.While.test, body, 1)) {
        return 0;
    }

    compiler_pop_fblock(c, WHILE_LOOP, loop);

    compiler_use_next_block(c, anchor);
    if (s->v.While.orelse) {
        VISIT_SEQ(c, stmt, s->v.While.orelse);
    }
    compiler_use_next_block(c, end);

    return 1;
}

static int
compiler_dowhile(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *body, *end, *anchor = NULL;
    loop = compiler_new_block(c);
    body = compiler_new_block(c);
    anchor = compiler_new_block(c);
    end = compiler_new_block(c);
    if (loop == NULL || body == NULL || anchor == NULL || end == NULL) {
        return 0;
    }
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, WHILE_LOOP, loop, end, NULL)) {
        return 0;
    }

    #if 1
    compiler_use_next_block(c, body);
    VISIT_SEQ(c, stmt, s->v.DoWhile.body);
    SET_LOC(c, s);
    if (!compiler_jump_if(c, s->v.DoWhile.test, anchor, 0)) {
        return 0;
    }
    if (!compiler_jump_if(c, s->v.DoWhile.test, body, 1)) {
        return 0;
    }
    #endif
    
    #if 0
    if (!compiler_jump_if(c, s->v.DoWhile.test, anchor, 0)) {
        return 0;
    }
    compiler_use_next_block(c, body);
    VISIT_SEQ(c, stmt, s->v.DoWhile.body);
    SET_LOC(c, s);
    if (!compiler_jump_if(c, s->v.DoWhile.test, body, 1)) {
        return 0;
    }
    #endif

    compiler_pop_fblock(c, WHILE_LOOP, loop);

    compiler_use_next_block(c, anchor);
    if (s->v.DoWhile.orelse) {
        VISIT_SEQ(c, stmt, s->v.DoWhile.orelse);
    }
    compiler_use_next_block(c, end);

    return 1;
}

static int
compiler_return(struct compiler *c, stmt_ty s)
{

再ビルド

変更が終わったので, ビルドします.

$ CFLAGS=-O0 -g" ./configure --prefix=/home/denjo/python/hkatori/cpython/cpython_install1
$ make
$ make install

エラー出力や警告などは出ず, ビルドすることができました.

結果

テストコード

以下のようなテストコードを作成しました.

#do-while文を実装する

#whileとdo-whileの違いがわかるケース
print("do-while loop")
a1 = 5
dowhile (a1 > 5): print("a1 is {0}".format(a1)) #出力される

print("while loop")
a2 = 5
while (a2 > 5): print("a2 is {0}".format(a2)) #出力されない

#do-whileがwhile文としての挙動も示すか確認するケース
print("======================")
b1 = 5
dowhile(b1 > 0):
    print("b1 is {0}".format(b1)) #5, 4, 3, 2, 1が出力
    if (b1 < 3): print("{0} is smaller than 3".format(b1)) #2, 1が出力
    b1 -= 1
else: print("while else is OK") #出力される

1つ目のループでは, do-whileは条件判定を最後に行うので, 5だけが出力されるはずです. 2つ目のループではwhile文なので, 条件判定は最初に行われるからwhile文内の処理は実行されません. 3つ目のループは繰り返し処理が実行されているかを確認する目的と, orelseが機能しているかを確認する目的があります.

実行

cpython/cpython_install1/binディレクトリに遷移します. 先程のpythonのテストケースをtestcase.pyと名づけ, cpython/cpython_install1/binに配置します. 実行すると以下のようになり, 正しく動作していることがわかります.

$ ./python3 testcase.py
do-while loop
a1 is 5
while loop
======================
b1 is 5
b1 is 4
b1 is 3
b1 is 2
2 is smaller than 3
b1 is 1
1 is smaller than 3
while else is OK

f:id:aiutarsi:20211029211604g:plain

なお, 以下でも動作します.

$ ./python3.11 testcase.py
$ python3 testcase.py

参考にした資料

以下, 本実験において, 参考にした資料を列挙します.

本実験のHP

https://doss.eidos.ic.i.u-tokyo.ac.jp

python公式ドキュメント

pythonと格闘した先輩方の記事

どのように改変すれば良いか, 様々な指針を与えてもらいました. ありがとうございます.

以上, 参考にしたブログなどを記載しました. ここに掲載しなかった他の方のブログもありました.

感想

正直, 最初は膨大なファイルサイズのソフトウェアやC言語で書かれているにしろpythonの言語処理系を手探るということで自分にできるのか不安なところがありました. デバッガでただひたすらブレークポイントを立てて, 関数の中に入って, 出力を確認するという作業を延々とこなしても問題解決の糸口が見られず焦りが生じました. Python Developer's Guideや過去の先輩方のブログを読むことを通じてこのフォルダやファイルは全体から見てどのような処理をしているのか, 文法を変更するにはこのファイルを変更するのが良いなどの情報を得ることができ, 全体像を概ね把握することにより, 改変は進んでいきました. このブログを読む限りでは, なんの苦労もなく, 目的のファイルに一目散に辿り着き, コードの中身も把握して必要な変更や追記をしているように見えるかもしれませんが, 実際はそうではなく公式ドキュメントと格闘しながら, 特に最後のcompile.cではcompiler_while関数を真似たcompiler_dowhile関数を弄ってはビルドして出力を確認するという行為を繰り返しています. 最後に, 本実験において, 田浦先生, TAの方々には詰まったりしたところで色々と教えていただきました. ありがとうございました.