こんにちは、鈴木です。
前回作成した 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] |
まとめ
何とか独自のワードを定義できるようになりました。次は条件分岐も実装してみたいと思います。