PythonでForth処理系を書く! その5(Read-Eval-Print-Loop)
REPL機能を実装する。
main.pyに引数がなければREPLを実行するようにする(引数としてファイル名が渡されていればそのファイルを実行)。REPLは一行ずつユーザからForthコードを受け取り、今まで受け取ったコードの環境を引き継ぎつつ実行する。
変更はcompiler、interpreter、そしてmainの三箇所。
compiler.py
compile関数が複数回呼ばれることを前提に以下の変更を加える:
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を追加する。