diff --git a/src/rforth.rb b/src/rforth.rb index 2e09e8a..76d2700 100644 --- a/src/rforth.rb +++ b/src/rforth.rb @@ -1,5 +1,74 @@ require 'pp' +module PrimitiveWords + + def dup + @stack << @stack.last + end + + def q_dup + @stack << @stack.last unless @stack.last == 0 + end + + def drop + @stack.pop + end + + def swap + @stack += [@stack.pop, @stack.pop] + end + + def over + a = @stack.pop + b = @stack.pop + @stack << b << a << b + end + + def rot + a = @stack.pop + b = @stack.pop + c = @stack.pop + @stack << b << a << c + end + + def plus + @stack << (@stack.pop + @stack.pop) + end + + def mult + @stack << (@stack.pop * @stack.pop) + end + + def subtract + a = @stack.pop + b = @stack.pop + @stack << b - a + end + + def divide + a = @stack.pop + b = @stack.pop + @stack << b / a + end + + def dot + @s_out.print( @stack.pop ) + end + + def cr + @s_out.puts + end + + def dot_s + @s_out.print( "#{@stack}\n" ) + end + + def dot_d + pp @dictionary + end + +end + class Dictionary def initialize( &block ) @entries = {} @@ -16,64 +85,85 @@ class Dictionary self end + def alias_word( name, old_name ) + entry = self[old_name] + raise "No such word #{old_name}" unless entry + new_entry = entry.dup + new_entry[:name] = name + @entries[name] = entry + end + def []( name ) @entries[name] end end class RForth + include PrimitiveWords def initialize( s_in = $stdin, s_out = $stdout ) @s_in = s_in @s_out = s_out - @dictionary = initial_dictionary + @dictionary = Dictionary.new @stack = [] + initialize_dictionary end - def initial_dictionary - Dictionary.new do |d| - d.word('dup') { @stack << @stack.last } - d.word('?dup') { @stack << @stack.last unless @stack.last == 0 } - d.word('drop') { @stack.pop } - d.word('swap') { @stack += [@stack.pop, @stack.pop] } - d.word('over') do - a = @stack.pop - b = @stack.pop - @stack << b << a << b - end - d.word('rot') do - a = @stack.pop - b = @stack.pop - c = @stack.pop - @stack << b << a << c - end - d.word(':') { define_word } - d.word('+') { @stack << (@stack.pop + @stack.pop) } - d.word('*') { @stack << (@stack.pop * @stack.pop) } - d.word('-') do - a = @stack.pop - b = @stack.pop - @stack << b - a - end - d.word('/') do - a = @stack.pop - b = @stack.pop - @stack << b / a - end - d.word('.') { @s_out.print( @stack.pop ) } - d.word('.S') { @s_out.print( "#{@stack}\n" ) } - d.word('.D') { pp @dictionary } - d.word('cr') { @s_out.puts } - d.word('bye') { exit } + # Create all of the initial words. + def initialize_dictionary + PrimitiveWords.public_instance_methods(false).each do |m| + method_clojure = method(m.to_sym) + word( m.to_s, &method_clojure ) end + + alias_word( '?dup', 'q_dup' ) + alias_word( '+', 'plus' ) + alias_word( '*', 'mult' ) + alias_word( '-', 'subtract' ) + alias_word( '/', 'divide' ) + alias_word( '.', 'dot' ) + alias_word( '.S', 'dot_s' ) + alias_word( '.D', 'dot_d' ) + + word(':') { read_and_define_word } + word('bye') { exit } + + immediate_word( '\\' ) { @s_in.readline } end - def define_word - name = read_word + # Convience method that takes a word and a closure + # and defines the word in the dictionary + def word( name, &block ) + @dictionary.word( name, &block ) + end + + # Convience method that takes a word and a closure + # and defines an immediate word in the dictionary + def immediate_word( name, &block ) + @dictionary.immediate_word( name, &block ) + end + + # Convience method that takes an existing dict. + # word and a new word and aliases the new word to + # the old. + def alias_word( name, old_name ) + @dictionary.alias_word( name, old_name ) + end + + # Given the name of a new words and the words + # that make up its definition, define the + # new word. + def define_word( name, *words ) + @dictionary.word( name, &compile_words( *words ) ) + end + + # Give an array of (string) words, return + # A block which will run all of those words. + # Executes all immedate words, well, immediately. + def compile_words( *words ) blocks = [] - while (word = read_word) - break if word == ';' - entry = @dictionary[word] + words.each do |word| + entry = resolve_word( word ) raise "no such word: #{word}" unless entry if entry[:immediate] entry[:block].call @@ -81,22 +171,49 @@ class RForth blocks << entry[:block] end end - - @dictionary.word(name) do + proc do blocks.each {|b| b.call} end end + # Read a word definition from input and + # define the word + # Definition looks like: + # new-word w1 w2 w3 ; + def read_and_define_word + name = read_word + words = [] + while (word = read_word) + break if word == ';' + words << word + end + @dictionary.word(name, &compile_words( *words )) + end + + # Given a (string) word, return the dictionary + # entry for that word or nil. + def resolve_word( word ) + return @dictionary[word] if @dictionary[word] + x = to_number(word) + if x + block = proc { @stack << x } + return { :name => word, :block => block, :immediate => false } + end + nil + end + + # Evaluate the given word. def forth_eval( word ) - if @dictionary[word] - @dictionary[word][:block].call - elsif (x = to_number(word)) - @stack << x + entry = resolve_word(word) + if entry + entry[:block].call else @s_out.puts "#{word} ??" end end + # Try to turn the word into a number, return nil if + # conversion fails def to_number( word ) begin return Integer( word ) @@ -111,15 +228,11 @@ class RForth nil end - def read_char - @s_in.readchar - end - def read_word result = nil ch = nil until @s_in.eof? - ch = read_char + ch = @s_in.readchar if result and is_space?(ch) break elsif result.nil?