commit ece6e2f1880e52e98abcb7028e82b58c437724d8 Author: Russ Olsen Date: Thu Aug 20 12:10:32 2015 -0400 Initial checkin of Crystal port diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b70d784 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.deps/ +/libs/ +/.crystal/ +/doc/ +crforth + + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c59f98e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: c +before_install: | + curl http://dist.crystal-lang.org/apt/setup.sh | sudo bash + sudo apt-get -q update + +install: | + sudo apt-get install crystal + +script: + - crystal spec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9911c43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Russ Olsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e4c0641 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +CR_FILES := $(wildcard src/*.cr) $(wildcard src/**/*.cr) + +crforth: $(CR_FILES) + crystal build -o $@ src/main.cr + +run: + crystal src/main.cr + +clean: + rm -f crforth diff --git a/Projectfile b/Projectfile new file mode 100644 index 0000000..db1b48f --- /dev/null +++ b/Projectfile @@ -0,0 +1,2 @@ +deps do +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..46c247c --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# crforth + +A simple FORTH interpreter (http://github.com/russolsen/rforth) ported +to the Crystal programming language. + +Note that CRForth is an experiment in porting a non-trivial application +from Ruby to Crystal. The code is, as I write this, just barely working. +This is probably not idiomatic Crystal -- I'm still figuring out what that +means. + +Some lessons so far: + +* Most of the effort of the port involved minor changes to make the static typing +happy. For example, FORTH interpreter uses a lot of procs whose return values are ignored. +Eventually I had all of them return nil to make the static typing happy. I'm not sure +if this is really the correct thing, but it was expediant. + +* The original Ruby version used metaprogramming to look at the methods available +in a module. I've done that by hand in CRForth because I don't see the equivalent +in Crystal. + +* It is really cool to get a stand alone, binary executable from Rubyish code. + +## Installation + +Add it to `Projectfile` + +```crystal +deps do + github "[your-github-name]/crforth" +end +``` + +## Usage + +```crystal +require "crforth" + +i = CRForth::Interpreter.new +i.run +``` + +Or just run the interpreter from source: + +```crystal +crystal src/main.cr +``` + +Right now CRForth doesn't have a great interface: It just silently prompts for +some FORTH code and executes it. To add 2 + 2 you would do the following: + +``` +~/projects/crystal/crforth: make run +crystal src/main.cr +2 2 + . cr +4 +bye +```` + +## Development + +There is a Makefile for convience. The targets are: + +* crforth: Build the executable. This is the default. + +* clean: Clean up any generated files. + +* run: Runs the interpeter from source. + +## Contributing + +1. Fork it ( https://github.com/[your-github-name]/edn/fork ) +2. Create your feature branch (git checkout -b my-new-feature) +3. Commit your changes (git commit -am 'Add some feature') +4. Push to the branch (git push origin my-new-feature) +5. Create a new Pull Request + +## Contributors + +- [russolsen](https://github.com/[russolsen]) Russ Olsen - creator, maintainer diff --git a/spec/crforth_spec.cr b/spec/crforth_spec.cr new file mode 100644 index 0000000..e4ecaaa --- /dev/null +++ b/spec/crforth_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Crforth do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..58d6fe0 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/crforth" diff --git a/src/crforth.cr b/src/crforth.cr new file mode 100644 index 0000000..14f66f9 --- /dev/null +++ b/src/crforth.cr @@ -0,0 +1,4 @@ +require "./crforth/*" + +module CRForth +end diff --git a/src/crforth/dictionary.cr b/src/crforth/dictionary.cr new file mode 100644 index 0000000..3d4b2d5 --- /dev/null +++ b/src/crforth/dictionary.cr @@ -0,0 +1,51 @@ +module CRForth + class Entry + property! name, block, immediate + + def initialize(name: String, block: Proc(Nil), immediate: Bool) + @name = name + @block = block + @immediate = immediate + end + + def dup + Entry.new(@name, @block, @immediate) + end + end + + class Dictionary + def initialize + @entries = {} of String => Entry + end + + def word( name: String , &block: -> Nil ) + @entries[name] = Entry.new(name, block, false) + nil + end + + def immediate_word( name: String , &block: -> Nil ) + @entries[name] = Entry.new(name, block, true) + end + + def alias_word( name: String, old_name: String ): Bool + entry = @entries[name] + #raise "No such word #{old_name}" unless entry + if ! entry.is_a?(Nil) + new_entry = entry.dup + new_entry.name = name + @entries[name] = entry + true + else + false + end + end + + def get( name ): Entry? + # puts "Looking up #{name} in entries" + # puts @entries.keys + result = @entries[name]? + # puts "result: #{result.class}" + result + end + end +end diff --git a/src/crforth/interpreter.cr b/src/crforth/interpreter.cr new file mode 100644 index 0000000..3eaa22a --- /dev/null +++ b/src/crforth/interpreter.cr @@ -0,0 +1,280 @@ +require "./dictionary" + +module CRForth + alias Num = (Int32 | Float64) + alias Anything = (Int32 | String | Float64 | Symbol | Nil) + + module PrimitiveWords + def dup + @stack << @stack.last + nil + end + + def q_dup + @stack << @stack.last unless @stack.last == 0 + nil + end + + def drop + @stack.pop + nil + end + + def swap + @stack += [@stack.pop, @stack.pop] + nil + end + + def over + a = @stack.pop + b = @stack.pop + @stack << b << a << b + nil + end + + def rot + a = @stack.pop + b = @stack.pop + c = @stack.pop + @stack << b << a << c + nil + end + + def plus + a = @stack.pop as Num + b = @stack.pop as Num + @stack << a + b + nil + end + + def mult + a = @stack.pop as Num + b = @stack.pop as Num + @stack << a*b + nil + end + + def subtract + a = @stack.pop as Num + b = @stack.pop as Num + @stack << b - a + nil + end + + def divide + a = @stack.pop as Num + b = @stack.pop as Num + @stack << b / a + nil + end + + def dot + @s_out.print( @stack.pop ) + nil + end + + def cr + @s_out.puts + nil + end + + def dot_s + @s_out.print( "#{@stack}\n" ) + nil + end + + def dot_d + pp @dictionary + nil + end + + def hello + puts "hello" + nil + end + end + + class Interpreter + include PrimitiveWords + + def initialize( s_in = STDIN, s_out = STDOUT ) + @s_in = s_in + @s_out = s_out + @dictionary = Dictionary.new + @stack = [] of Anything + initialize_dictionary + end + + # Create all of the initial words. + def initialize_dictionary + word(":") do + read_and_define_word + end + + immediate_word( "\\" ) { @s_in.read_line; nil } + word("bye"){ exit } + word("+") {plus} + word(".") {dot} + word(".d") {dot_d} + word(".s") {dot_s} + word("*") {mult} + word("-") {subtract} + word("/") {divide} + word("cr") {cr} + word("hello") {hello} + end + + # Convience method that takes a word and a closure + # and defines the word in the dictionary + def word( name, &block: -> Nil ) + @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: -> Nil ) + @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: String, old_name: String ) + @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: String , words: Array(String) ) + @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: Array(String) ) + blocks = [] of Proc(Nil) + words.each do |word| + entry = resolve_word( word ) + raise "no such word: #{word}" unless entry + if entry.immediate + entry.block.call + else + blocks << entry.block + end + end + Proc(Nil).new{blocks.each {|b| b.call}; nil} + 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 = [] of String + while (word = read_word) + break if word == ";" + words << word + end + unless name.is_a?(Nil) + @dictionary.word(name, &compile_words(words)) + end + end + + # Given a (string) word, return the dictionary + # entry for that word or nil. + def resolve_word( word: String ): Entry | Nil + return @dictionary.get(word) unless @dictionary.get(word).is_a?(Nil) + x = to_number(word) + unless x.is_a?(Nil) + block = ->{push_number(x)} + return Entry.new(word, block, false) + end + nil + end + + def push_number(x: Nil | Int32 | Float64) + @stack << x + nil + end + + # Evaluate the given word. + def forth_eval( word: String ) + entry = resolve_word(word) + if entry.is_a?(Nil) + @s_out.puts "#{word} ??" + elsif entry.block.is_a?(Nil) + @s_out.puts "#{word} ??" + else + entry.block.call + end + end + + def forth_eval(word: Nil) + puts "Cant evaluate nil" + end + + def to_float(word : String) : Float64? + begin + word.to_f + rescue + nil + end + end + + def to_int(word : String) : Int32? + begin + word.to_i + rescue + nil + end + end + + # Try to turn the word into a number, return nil if + # conversion fails + def to_number( word ) + if word.is_a?(Nil) + return nil + elsif (x = to_int(word)) + return x + elsif (x = to_float(word)) + return x + end + end + + def read_word: String? + result = nil + ch = nil + while true + start_line = false + + ch = @s_in.read_char + if ch.is_a?(Nil) + break + elsif result && ch.whitespace? + break + elsif result.is_a?(Nil) + result = ch.to_s + else + result += ch + end + end + return result if result + nil + end + + def read_string: String + end + + def run + while true + word = read_word + break unless word + forth_eval( word ) + @s_out.flush + end + end + end +end diff --git a/src/crforth/version.cr b/src/crforth/version.cr new file mode 100644 index 0000000..6c8c02c --- /dev/null +++ b/src/crforth/version.cr @@ -0,0 +1,3 @@ +module CRForth + VERSION = "0.0.1" +end diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..760cb9b --- /dev/null +++ b/src/main.cr @@ -0,0 +1,3 @@ +require "./crforth" + +CRForth::Interpreter.new.run