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