こんにちは、鈴木です。
前回作成した Forth インタプリタを改良して、ワードの定義などができるようにしたいと思います。
元になるコード
元になるコードはこちらです。前回作成したものに定義済みワードを追加したものです。(@words にいくつか追加しました。)
| 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 | class MyForth   def initialize     @stack = []     @words = {       '.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) },       '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)} },     }   end   def eval_word(word)     if word =~ /^^?\d+$/       @stack.push(word.to_i)     elsif @words[word]       @words[word].call     else       puts "ERROR: Unsupported word: #{word}"     end   end end | 
ワードを定義できるようにしたい
ワードの定義は次のような書き方をするのでした。
| 1 | : ワード名 ... ; | 
例えば値を 3 乗する cube というワードは次のように定義できます。
| 1 | : cube dup dup * * ; | 
これを実現するにはどうしたら良いでしょう。
以下のような処理を追加すれば良さそうですね。
- 「:」が入力された場合、それ以降の「;」以外の入力は評価せずにスタックに積む。
- 「;」が入力された場合、「:」から「;」までを実行する Proc を作成し、@words に登録する。
現状の eval_word(word) メソッドをそのまま使うとワードがすぐに評価されてしまうので、1. を実現することができません。
そのため eval_word(word) の処理内容を「ワード定義外」と「ワード定義内」で処理を分けるようにする必要があります。
それに伴い、ワード定義外とワード定義内で使用可能なワードも切り替える必要があります(例えばワード定義外では「;」は使えないが、ワード定義内では「;」を使えるなど)。
修正方針を整理すると、
- eval_word(word) の処理内容を「ワード定義外」の場合と「ワード定義内」の場合で切り替える。
- 使えるワードも「ワード定義外」の場合と「ワード定義内」の場合で切り替える(@words を分割する)。
- 「;」が入力されたら「:」から「;」までの内容を実行する Proc を作成し、定義済みワードとして登録する。
となります。
修正版のコード
上記の方針で修正したコードは以下の通りです。
| 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 | class MyForth   def initialize     @stack = []     # (1) ワード定義外で使用可能なワード.     @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)} },       # (1) 追加.       ':'    => method(:begin_word_definition),     }     # (1) ワード定義内で使用可能なワード.     @words_in_definition = {       # (1) 追加.       ';'    => method(:end_word_definition),     }     # (2) eval_word(word) メソッドが内部で呼び出すメソッド.     @eval_word = method(:eval_word_in_default)   end   # (2) ワードを評価する.   def eval_word(word)     @eval_word.call(word)   end   private   # (2) ワードを評価する (ワード定義外にいる場合).   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   # (2) ワードを評価する (ワード定義内にいる場合).   def eval_word_in_definition(word)     if @words_in_definition[word]       @words_in_definition[word].call     else       @stack.push(word)     end   end   # (3) ワード定義内に入ったときの処理.   def begin_word_definition     @stack.push(':')     @eval_word = method(:eval_word_in_definition)   end   # (4) ワード定義が終わったときの処理.   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 | 
色々変更していますが、ポイントをコメントで記載しています。
まずは (1) で元のコードにおける @words を @words_in_default(ワード定義外で使用可能なワード)と @word_in_definition(ワード定義内で使用可能なワード)に分割し、それぞれ「:」と「;」の処理を行う Proc を追加しています。
次に (2) の部分を見てください。eval_word(word) メソッドを eval_word_in_default(word) と eval_word_in_definition(word) に分割し、呼び出すべきメソッドを @eval_word に保持するように変更しました。
(3) の begin_word_definition メソッドは「:」が入力されたときに呼び出されます。内部ではスタックに「:」を push してから、以降の eval_word(word) 呼び出しで eval_word_in_definition(word) が呼び出されるように @eval_word の値を更新しています。
(4) の end_word_definition メソッドは、ワード定義の本体です。「:」の後に入力された値が @stack に積まれているので、それを分解して Proc オブジェクトに変換し、登録済みのワードに追加(@word_in_default に追加)しています。最後に @eval_word の値を元に戻しています。
動かしてみる
それでは動かしてみましょう。
前回同様、次のような足場を作って実行すれば REPL を始めることができます。
| 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 | 
やろうとしていた、値を 3 乗するワード cube を定義してみます。
| 1 | : cube dup dup * * ; | 
3 の 3 乗を求めてみると・・、
| 1 | 3 cube .s | 
無事に 27 と表示されました。
| 1 | [27] | 
まとめ
何とか独自のワードを定義できるようになりました。次は条件分岐も実装してみたいと思います。

 
						