Arantium Maestum

プログラミング、囲碁、読書の話題

PythonでForth処理系を書く! その5(Read-Eval-Print-Loop)

REPL機能を実装する。

main.pyに引数がなければREPLを実行するようにする(引数としてファイル名が渡されていればそのファイルを実行)。REPLは一行ずつユーザからForthコードを受け取り、今まで受け取ったコードの環境を引き継ぎつつ実行する。

github.com

変更はcompiler、interpreter、そしてmainの三箇所。

compiler.py

compile関数が複数回呼ばれることを前提に以下の変更を加える:

  • 中間言語に変換するtokensを初期化せずに追加するようにする
  • 中間言語のcodesから'end'コードを取り除く
  • 'end'フラグをFalseにセットする
def compile(state, tokens):
    state['tokens'].extend(tokens)
    if state['codes']:
        state['codes'].pop()
    state['end'] = False

    while not state['end']:
        run(state)
    return state['codes']

どのトークンまで変換したかを表す'pos'値は以前の最後のトークンの一つ先を指しているので、ちょうど新たに追加したトークンの先頭を指すことになる。

なのでこれでcompile関数が走れば追加分のトークンだけ中間言語に変換してくれる。

interpreter.py

変更はinterpret関数のstate['end']をFalseにセットするだけ。

def interpret(state, codes):
    state['codes'] = codes
    state['end'] = False #ここだけ変更

    while not state['end']:
        run(state)

main.py

  • 今までのmain部分をrun_fileという関数に
  • run_repl関数を別途定義
  • main部分ではスクリプトが呼ばれたときにargvに引数が渡されたかどうかでrun_fileかrun_replにディスパッチ

今回追加したrun_repl関数はこんな感じ:

def run_repl():
    compiler_state = compiler.init_state()
    interpreter_state = interpreter.init_state()

    print("Starting Forth REPL...")

    while True:
        s = input('> ')
        if s == 'QUIT': break

        tokens = tokenizer.tokenize(s)
        codes = compiler.compile(compiler_state, tokens)
        interpreter.interpret(interpreter_state, codes)

まずは内部状態を初期化して、ループ内で新しくユーザから受け取った文字列をトークン化し、内部状態と一緒に引数としてcompile/interpretに渡して処理している。

実行

これで単にpython main.pyと呼び出せば:

Starting Forth REPL...
> 3 4 +
> 5 6 +
> + . CR
18

というような感じで対話的にForthを書ける。行の間で環境(とくにスタックの状態)が引き継がれているのがわかる。

これで機能を追加したときにチェック・デバッグしやすくなった。

次回はI分岐構文のIF-THEN-ELSEを追加する。