こんにちは、鈴木です。
興味本位で作り始めた Forth インタプリタの続きです。
前回はワードを定義できるようにしたので、今度は条件分岐をサポートしようと思います。
条件分岐の書き方(おさらい)
条件分岐の書き方ですが、
| 1 | if 真の場合 then | 
または、
| 1 | if 真の場合 else 偽の場合 then | 
と書くのでした。
また、条件分岐はワードの定義内(「:」から「;」までの間)でしか書けない、ということもポイントです。
例えば(スタックの一番上にある)値が負数の場合は 0 にする、というワードは次のように定義できます。
| 1 | : f dup 0 < if drop 0 then ; | 
if ~ then をネストすることもできるので、値の符号を求めるワード(負数なら -1、正数なら 1、0 なら 0 とするワード)は次のように定義できます。
| 1 | : sign dup 0 < if -1 else dup 0 > if 1 else 0 then then ; | 
前回書いたコード
前回書いたコードを再掲します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | class MyForth   def initialize     @stack = []     # ワード定義外で使用可能なワード.     @words_in_default = {       '.s'   => lambda { p @stack },       '+'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs + rhs) },       '-'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs - rhs) },       '*'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs * rhs) },       '/'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs / rhs) },       '.'    => lambda { @stack.pop },       'dup'  => lambda { @stack.push(@stack.last) },       'drop' => lambda { @stack.pop },       'swap' => lambda { lhs, rhs = @stack.pop(2); @stack.push(rhs, lhs) },       'over' => lambda { @stack.push(@stack[-2]) },       'rot'  => lambda { @stack.push(@stack.delete_at(-3)) },       'nip'  => lambda { %w(swap drop).each{|word| eval_word(word)} },       'tuck' => lambda { %w(swap over).each{|word| eval_word(word)} },       ':'    => method(:begin_word_definition),     }     # ワード定義内で使用可能なワード.     @words_in_definition = {       ';'    => method(:end_word_definition),     }     # eval_word(word) メソッドが内部で呼び出すメソッド.     @eval_word = method(:eval_word_in_default)   end   # ワードを評価する.   def eval_word(word)     @eval_word.call(word)   end   private   # ワードを評価する (ワード定義外にいる場合).   def eval_word_in_default(word)     if word =~ /^-?\d+$/       @stack.push(word.to_i)     elsif @words_in_default[word]       @words_in_default[word].call     else       puts "ERROR: Unsupported word: #{word}"     end   end   # ワードを評価する (ワード定義内にいる場合).   def eval_word_in_definition(word)     if @words_in_definition[word]       @words_in_definition[word].call     else       @stack.push(word)     end   end   # ワード定義内に入ったときの処理.   def begin_word_definition     @stack.push(':')     @eval_word = method(:eval_word_in_definition)   end   # ワード定義が終わったときの処理.   def end_word_definition     # スタック上のワード定義を分解する.     # Ex. [..., ':', 'square', 'dup', '*'] を name='square', definitions=['dup', '*'] に分解する.     name, *definitions = @stack.slice!(@stack.rindex(':') .. -1).drop(1)     @words_in_default[name] = lambda { definitions.each{|word| eval_word_in_default(word)} }     @eval_word = method(:eval_word_in_default)   end end | 
前回は単純な計算しかできない状態からワード定義ができるように改良しました。
実装面では、ワードを評価するメソッド eval_word(word) 内部で、ワード定義内にいる時は eval_word_in_definition(word) を呼ぶ、ワード定義の外側にいるときは eval_word_in_default(word) を呼ぶように変更しました(どちらを呼び出すかは @eval_word に Method オブジェクトを代入しておきました)。
今回の修正方針
今回は条件分岐をサポートします。
条件分岐はワード定義内(「:」から「;」までの間)でしか書けないため、eval_word_in_definition(word) メソッドでうまいこと対応したいところです。
ということで修正版のコードです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | class MyForth   def initialize     @stack = []     # ワード定義外で使用可能なワード.     @words_in_default = {       '.s'   => lambda { p @stack },       '+'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs + rhs) },       '-'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs - rhs) },       '*'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs * rhs) },       '/'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs / rhs) },       '.'    => lambda { @stack.pop },       '='    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs == rhs) },       '<>'   => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs == rhs) },       '<'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs < rhs) },       '<='   => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs <= rhs) },       '>'    => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs > rhs) },       '>='   => lambda { lhs, rhs = @stack.pop(2); @stack.push(lhs >= rhs) },       'dup'  => lambda { @stack.push(@stack.last) },       'drop' => lambda { @stack.pop },       'swap' => lambda { lhs, rhs = @stack.pop(2); @stack.push(rhs, lhs) },       'over' => lambda { @stack.push(@stack[-2]) },       'rot'  => lambda { @stack.push(@stack.delete_at(-3)) },       'nip'  => lambda { %w(swap drop).each{|word| eval_word(word)} },       'tuck' => lambda { %w(swap over).each{|word| eval_word(word)} },       ':'    => method(:begin_word_definition),     }     # ワード定義内で使用可能なワード.     @words_in_definition = {       ';'    => method(:end_word_definition),       # (1) 追加.       'if'   => method(:begin_if_definition),       'then' => method(:end_if_definition),     }     # eval_word(word) メソッドが内部で呼び出すメソッド.     # (2) if-then はネストする場合があるため配列に変更 (スタックとして使う).     @eval_word = [method(:eval_word_in_default)]   end   def eval_word(word)     # (2) 変更.     @eval_word.last.call(word)   end   private   def eval_word_in_default(word)     if word =~ /^-?\d+$/       @stack.push(word.to_i)     elsif @words_in_default[word]       @words_in_default[word].call     else       puts "ERROR: Unsupported word: #{word}"     end   end   def eval_word_in_definition(word)     if @words_in_definition[word]       @words_in_definition[word].call     else       @stack.push(word)     end   end   def begin_word_definition     @stack.push(':')     # (2) 変更.     @eval_word.push(method(:eval_word_in_definition))   end   def end_word_definition     # スタック上のワード定義を分解する.     # Ex. [..., ':', 'square', 'dup', '*'] を name='square', definitions=['dup', '*'] に分解する.     name, *definitions = @stack.slice!(@stack.rindex(':') .. -1).drop(1)     @words_in_default[name] = lambda { definitions.each{|word| eval_word_in_default(word)} }     # (2) 変更.     @eval_word.pop   end   # (3) 追加.   def begin_if_definition     @stack.push('if')     @eval_word.push(method(:eval_word_in_definition))   end   # (3) 追加.   def end_if_definition     # スタック上のワード定義を分解する.     # Ex. [..., 'if', T1, T2, ..., 'else', F1, F2, ..., 'then'] を true_case=[T1, T2, ...], false_case=[F1, F2, ...] に分解する.     definitions = @stack.slice!(@stack.rindex('if') .. -1).drop(1)     true_case = definitions.take_while{|word| word != 'else'}     false_case = definitions.drop_while{|word| word != 'else'}.drop_while{|word| word == 'else'}     # 自身をキーとしてワードに追加する.     word = lambda{ (@stack.pop ? true_case : false_case).each{|word| eval_word_in_default(word)} }     @stack.push(word)     @words_in_default[word] = word     @eval_word.pop   end end | 
コメント中の (1) を見てください。条件分岐は if で始まり then で終わるので、@words_in_definition に if と then を追加しました。else は追加していませんが、それは then を見つけた段階で if から then の中を Proc オブジェクトに変換するため、@words_in_definition に追加して特別に処理する必要は無いからです。
次に (2) の部分ですが、@eval_word を配列に変更しました。というのも、if ~ else ~ then はネストする可能性があるため、単純に「then が来たら @eval_word に method(:eval_word_in_default) を代入」してしまうと正しく動かないためです。呼び出す側も「@eval_word.call(word)」から「@eval_word.last.call(word)」に変更しています。
(3) では if ~ then の部分を処理しています。begin_if_definition メソッドでは @stack に 'if' を積んでから @eval_word に method(:eval_word_in_definition) を追加しています。
end_if_definition メソッドでは、@stack から 'if' の定義を definitions に取り出し、条件が真の場合(true_case)と偽の場合(false_case)に分割しています。あとは Proc オブジェクトに変換してから @words_in_default に追加するだけです。
動かしてみる
それでは動かしてみます。
| 1 2 3 4 5 6 7 | forth = MyForth.new while line = gets   line.split(/\s/).each do |word|     forth.eval_word(word)   end end | 
最初に出てきた、値の符号を求めるワード sign を定義してみます。
| 1 | : sign dup 0 < if -1 else dup 0 > if 1 else 0 then then ; | 
適当な値で試してみます。
プラスの値だと 1 になる。
| 1 2 | 123 sign .s [123, 1] | 
0 だと 0 になる。
| 1 2 | 0 sign .s [0, 0] | 
マイナスの値だと -1 になる。
| 1 2 | -123 sign .s [-123, -1] | 
きちんと動きました。
次回はループをサポートしようと思います。

 
						