Initial checkin of Crystal port

This commit is contained in:
Russ Olsen 2015-08-20 12:10:32 -04:00
commit ece6e2f188
13 changed files with 482 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/.deps/
/libs/
/.crystal/
/doc/
crforth

10
.travis.yml Normal file
View file

@ -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

21
LICENSE Normal file
View file

@ -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.

10
Makefile Normal file
View file

@ -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

2
Projectfile Normal file
View file

@ -0,0 +1,2 @@
deps do
end

80
README.md Normal file
View file

@ -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

9
spec/crforth_spec.cr Normal file
View file

@ -0,0 +1,9 @@
require "./spec_helper"
describe Crforth do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end

2
spec/spec_helper.cr Normal file
View file

@ -0,0 +1,2 @@
require "spec"
require "../src/crforth"

4
src/crforth.cr Normal file
View file

@ -0,0 +1,4 @@
require "./crforth/*"
module CRForth
end

51
src/crforth/dictionary.cr Normal file
View file

@ -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

280
src/crforth/interpreter.cr Normal file
View file

@ -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

3
src/crforth/version.cr Normal file
View file

@ -0,0 +1,3 @@
module CRForth
VERSION = "0.0.1"
end

3
src/main.cr Normal file
View file

@ -0,0 +1,3 @@
require "./crforth"
CRForth::Interpreter.new.run