mirror of
https://github.com/russolsen/crforth
synced 2025-01-11 20:01:35 +01:00
Initial checkin of Crystal port
This commit is contained in:
commit
ece6e2f188
13 changed files with 482 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/.deps/
|
||||
/libs/
|
||||
/.crystal/
|
||||
/doc/
|
||||
crforth
|
||||
|
||||
|
10
.travis.yml
Normal file
10
.travis.yml
Normal 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
21
LICENSE
Normal 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
10
Makefile
Normal 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
2
Projectfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
deps do
|
||||
end
|
80
README.md
Normal file
80
README.md
Normal 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
9
spec/crforth_spec.cr
Normal 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
2
spec/spec_helper.cr
Normal file
|
@ -0,0 +1,2 @@
|
|||
require "spec"
|
||||
require "../src/crforth"
|
4
src/crforth.cr
Normal file
4
src/crforth.cr
Normal file
|
@ -0,0 +1,4 @@
|
|||
require "./crforth/*"
|
||||
|
||||
module CRForth
|
||||
end
|
51
src/crforth/dictionary.cr
Normal file
51
src/crforth/dictionary.cr
Normal 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
280
src/crforth/interpreter.cr
Normal 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
3
src/crforth/version.cr
Normal file
|
@ -0,0 +1,3 @@
|
|||
module CRForth
|
||||
VERSION = "0.0.1"
|
||||
end
|
3
src/main.cr
Normal file
3
src/main.cr
Normal file
|
@ -0,0 +1,3 @@
|
|||
require "./crforth"
|
||||
|
||||
CRForth::Interpreter.new.run
|
Loading…
Reference in a new issue