rbl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 79590c140a9864fb5f46c288fe46603d8b63b3c9
4
+ data.tar.gz: 2109a3b23bf7b20e8e30e95d8e0451119ec72a5a
5
+ SHA512:
6
+ metadata.gz: c66238bc2c4772dd3557edfd11a56309135e9f423361a8c9783b14193a75c7d1208da3005463d2fc5682e34714ac88ec748bcd3c61efb41611f24f6a12b9372c
7
+ data.tar.gz: 82e872d0e9e44745b075f5b2a26bf9fdebfcc6666fd7ac45c4cac2c13df81333e98952998a4b1e250b4e565e0ef8da3b60616364c292977fed10594fedf7b7d5
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ *.gem
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.14.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubylisp.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Dave Yarwood
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # RubyLisp
2
+
3
+ A Lisp dialect of Ruby.
4
+
5
+ ## Why?
6
+
7
+ Lisps are great, but I haven't found one usable enough (by my own standards) for scripting. [Clojure][clojure] is my favorite language from a design perpective, and the fact that you can leverage existing JVM libraries is super convenient. But there's just one problem -- its startup time is _so slow_.
8
+
9
+ [Common Lisp][commonlisp] is nice; it's a powerful Lisp, and it's fast. But it's not always
10
+ easy to find X existing library to do Y thing, it's maybe a little too
11
+ low-level for my liking, and the tooling situation is not so great.
12
+
13
+ [Ruby][ruby] is great for scripting, cross-platform, and has hella good libraries, but there's just one problem -- it's not a Lisp.
14
+
15
+ I've played around with the [make-a-lisp][mal] guide a few times in the past to
16
+ build Lisps for fun in languages like Rust. I thought it would be interesting to
17
+ try and build for myself the convenient scripting Lisp that I always wanted. It turns out that it's super easy to write a Lisp interpreter, but it's awful time-consuming to build a whole language from scratch. I had a shower thought that the Ruby standard library can already do about 80% of the things a faithful Lisp should do, so maybe the path of least resistance to building the Lisp of my dreams is to write the interpreter in Ruby and totally cheat on all of the function implementations by delegating to functions and types that have already been implemented in the Ruby stdlib.
18
+
19
+ I think I was right. Check it out, everyone -- you can write your Ruby scripts in Lisp now!
20
+
21
+ ## Features
22
+
23
+ > NOTE: RubyLisp is still in early development. If you find something is broken
24
+ > or missing, please file an issue, or better yet, fork this repo, add/fix it
25
+ > yourself, and make a Pull Request!
26
+
27
+ * Syntax and stdlib functions heavily influenced by Clojure.
28
+ * Easy, convenient Ruby inter-op.
29
+ * Fire up a REPL or run it in a script.
30
+ * Immutable linked lists, vectors, and hashes courtesy of the [Hamster][hamster]
31
+ library.
32
+ * _(TODO)_ Macros, Clojure-style.
33
+ * _(TODO)_ Namespaces, Clojure-style.
34
+ * _(TODO)_ Dependency management / the ability to use some sort of build tool to
35
+ include Ruby libraries and use them via inter-op.
36
+
37
+ ## Examples
38
+
39
+ _TODO_
40
+
41
+ ## Installation
42
+
43
+ Run this:
44
+
45
+ ```bash
46
+ $ gem install rubylisp
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ To start a REPL:
52
+
53
+ ```bash
54
+ $ rbl
55
+ ```
56
+
57
+ To interpret a file containing RubyLisp code:
58
+
59
+ ```bash
60
+ $ rbl my_sweet_rubylisp_script.rbl
61
+ ```
62
+
63
+ Or, if you'd like, you can include a shebang, make the script executable and run it directly:
64
+
65
+ ```
66
+ $ cat << EOF > reticulate_splines.rbl
67
+ #!/usr/bin/env rbl
68
+ (print "Reticulating splines... ")
69
+ (Kernel::sleep 2)
70
+ (println "done.")
71
+ EOF
72
+
73
+ $ chmod +x reticulate_splines.rbl
74
+
75
+ $ ./reticulate_splines.rbl
76
+ Reticulating splines... done.
77
+ ```
78
+
79
+ ## Development
80
+
81
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
82
+
83
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
84
+
85
+ ## Contributing
86
+
87
+ Contributions welcome!
88
+
89
+ As a general rule, I want RubyLisp to mirror the behavior of Clojure as closely
90
+ as possible. If you have a favorite Clojure function/macro and it isn't included
91
+ in RubyLisp yet, why not add it yourself and make a Pull Request? :)
92
+
93
+ ## License
94
+
95
+ Copyright © 2017 Dave Yarwood
96
+
97
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
98
+
99
+ [clojure]: https://clojure.org
100
+ [commonlisp]: https://en.wikipedia.org/wiki/Common_Lisp
101
+ [ruby]: http://ruby-lang.org
102
+ [mal]: https://github.com/kanaka/mal
103
+ [hamster]: https://github.com/hamstergem/hamster
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rubylisp"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/rbl ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'rubylisp/parser'
5
+ require 'rubylisp/repl'
6
+
7
+ USAGE = <<~HEREDOC
8
+ Usage:
9
+ To start a REPL session:
10
+ #{File.basename $0}
11
+
12
+ To run a file:
13
+ #{File.basename $0} <filename>
14
+ HEREDOC
15
+
16
+ if __FILE__ == $0
17
+ case ARGV.length
18
+ when 0
19
+ RubyLisp::REPL::start
20
+ when 1
21
+ file_contents = File.read ARGV[0]
22
+ input = file_contents.gsub(/\A#!.*\n/, '')
23
+ puts RubyLisp::Parser.parse(input)
24
+ else
25
+ puts USAGE
26
+ end
27
+ end
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,75 @@
1
+ require 'rubylisp/parser'
2
+
3
+ module RubyLisp
4
+ class Environment
5
+ attr_accessor :namespace, :vars, :outer
6
+
7
+ def initialize(outer: nil, namespace: nil)
8
+ @vars = {'*ns*' => (outer or self)}
9
+ @outer = outer
10
+ @namespace = namespace or
11
+ (outer.namespace if outer) or
12
+ "__ns_#{rand 10000}"
13
+ end
14
+
15
+ def set key, val
16
+ @vars[key] = val
17
+ end
18
+
19
+ def find key
20
+ if @vars.member? key
21
+ self
22
+ elsif @outer
23
+ @outer.find key
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def get key
30
+ env = find key
31
+ if env
32
+ env.vars[key]
33
+ else
34
+ raise RubyLisp::RuntimeError, "Unable to resolve symbol: #{key}"
35
+ end
36
+ end
37
+
38
+ # Copies all vars from `other_env` to this one.
39
+ def refer other_env
40
+ @vars = @vars.merge other_env.vars do |key, oldval, newval|
41
+ if key == '*ns*'
42
+ oldval
43
+ else
44
+ puts "WARNING: #{namespace}/#{key} being replaced by " +
45
+ "#{other_env.namespace}/#{key}"
46
+ newval
47
+ end
48
+ end
49
+ end
50
+
51
+ # TODO: some notion of "required namespaces" whose vars can be accessed when
52
+ # qualified with the namespace name
53
+
54
+ def load_rbl_file path
55
+ root = File.expand_path '../..', File.dirname(__FILE__)
56
+ input = File.read "#{root}/#{path}"
57
+
58
+ namespace = RubyLisp::Environment.new
59
+ RubyLisp::Parser.parse input, namespace
60
+ namespace
61
+ end
62
+
63
+ def stdlib
64
+ namespace = load_rbl_file 'rubylisp/core.rbl'
65
+ refer namespace
66
+ self
67
+ end
68
+
69
+ def repl
70
+ namespace = load_rbl_file 'rubylisp/repl.rbl'
71
+ refer namespace
72
+ self
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,227 @@
1
+ require 'rubylisp/environment'
2
+ require 'rubylisp/printer'
3
+ require 'rubylisp/types'
4
+
5
+ module RubyLisp
6
+ module Evaluator
7
+ module_function
8
+
9
+ def assert_number_of_args sexp, num_args
10
+ fn, *args = sexp.value
11
+ fn_name = fn.value
12
+ unless args.count == num_args
13
+ raise RubyLisp::RuntimeError,
14
+ "Wrong number of arguments passed to `#{fn_name}`; " +
15
+ "got #{args.count}, expected #{num_args}."
16
+ end
17
+ end
18
+
19
+ def assert_at_least_n_args sexp, num_args
20
+ fn, *args = sexp.value
21
+ fn_name = fn.value
22
+ unless args.count >= num_args
23
+ raise RubyLisp::RuntimeError,
24
+ "Wrong number of arguments passed to `#{fn_name}`; " +
25
+ "got #{args.count}, expected at least #{num_args}."
26
+ end
27
+ end
28
+
29
+ def assert_arg_type sexp, arg_number, arg_type
30
+ fn = sexp.value[0]
31
+ fn_name = fn.value
32
+ arg = if arg_number == 'last'
33
+ sexp.value.last
34
+ else
35
+ sexp.value[arg_number]
36
+ end
37
+
38
+ arg_description = if arg_number == 'last'
39
+ 'The last argument'
40
+ else
41
+ "Argument ##{arg_number}"
42
+ end
43
+
44
+ arg_types = if arg_type.class == Array
45
+ arg_type
46
+ else
47
+ [arg_type]
48
+ end
49
+
50
+ expected = case arg_types.count
51
+ when 1
52
+ arg_types.first
53
+ when 2
54
+ arg_types.join(' or ')
55
+ else
56
+ last_arg_type = arg_types.pop
57
+ arg_types.join(', ') + " or #{last_arg_type}"
58
+ end
59
+
60
+ if arg_types.none? {|type| arg.is_a? type}
61
+ raise RubyLisp::RuntimeError,
62
+ "#{arg_description} to `#{fn_name}` must be a " +
63
+ "#{expected}; got a #{arg.class}."
64
+ end
65
+ end
66
+
67
+ def eval_ast input, env
68
+ case input
69
+ when Array # of ASTs to be evaluated, e.g. multiple expressions in a file
70
+ input.compact.map {|form| eval_ast(form, env)}.last
71
+ when RubyLisp::HashMap
72
+ input.value.map {|k, v| [eval_ast(k, env), eval_ast(v, env)]}
73
+ when RubyLisp::List
74
+ if input.value.empty?
75
+ input.value
76
+ elsif input.value[0].value == 'apply'
77
+ coll = eval_ast(input.value[-1], env) || Hamster::Vector[]
78
+
79
+ # evaluate the last argument
80
+ sexp = RubyLisp::List.new(input.value.fill(coll, -1, 1))
81
+ # assert that it's enumerable
82
+ assert_arg_type sexp, 'last', Enumerable
83
+
84
+ # drop `apply` and pull the last form's contents into the sexp as
85
+ # multiple arguments
86
+ all_but_last_arg = sexp.value[1..-2].map {|arg| eval_ast arg, env}
87
+ fn, *args = all_but_last_arg + coll.to_list
88
+ sexp = RubyLisp::List.new([RubyLisp::Symbol.new("apply"), fn])
89
+ assert_arg_type sexp, 1, Proc
90
+ fn.call(*args)
91
+ elsif input.value[0].value == 'def'
92
+ assert_arg_type input, 1, RubyLisp::Symbol
93
+ key, val = input.value[1..-1]
94
+ env.set key.value, eval_ast(val, env)
95
+ elsif input.value[0].value == 'do'
96
+ body = input.value[1..-1]
97
+ body.map {|form| eval_ast(form, env)}.last
98
+ elsif input.value[0].value == 'fn'
99
+ if input.value[1].class == RubyLisp::Symbol
100
+ fn_name = input.value[1].value
101
+ bindings, *body = input.value[2..-1]
102
+ else
103
+ fn_name = "__fn_#{rand 10000}"
104
+ bindings, *body = input.value[1..-1]
105
+ end
106
+
107
+ unless bindings.class == RubyLisp::Vector
108
+ raise RubyLisp::RuntimeError,
109
+ "The bindings of `fn` must be a RubyLisp::Vector."
110
+ end
111
+
112
+ ampersand_indices = bindings.value.to_list.indices {|x| x.value == '&'}
113
+ if ampersand_indices.any? {|i| i != bindings.value.count - 2}
114
+ raise RubyLisp::RuntimeError,
115
+ "An '&' can only occur right before the last binding."
116
+ end
117
+
118
+ fn = lambda do |*args|
119
+ inner_env = RubyLisp::Environment.new(outer: env)
120
+ inner_env.set(fn_name, fn) # self-referential lambda omg
121
+
122
+ sexp = RubyLisp::List.new([RubyLisp::Symbol.new(fn_name), *args])
123
+ if bindings.value.any? {|binding| binding.value == '&'}
124
+ required_args = bindings.value.count - 2
125
+ assert_at_least_n_args sexp, required_args
126
+
127
+ bindings.value[0..-3].zip(args.take(required_args)).each do |k, v|
128
+ inner_env.set(k.value, v)
129
+ end
130
+
131
+ rest_args = if args.count > required_args
132
+ args[required_args..-1].to_list
133
+ else
134
+ nil
135
+ end
136
+
137
+ inner_env.set(bindings.value[-1].value, rest_args)
138
+ else
139
+ required_args = bindings.value.count
140
+ assert_number_of_args sexp, required_args
141
+
142
+ bindings.value.zip(args).each do |k, v|
143
+ inner_env.set(k.value, v)
144
+ end
145
+ end
146
+
147
+ body.map {|form| eval_ast(form, inner_env)}.last
148
+ end
149
+ elsif input.value[0].value == 'if'
150
+ cond, then_form, else_form = input.value[1..-1]
151
+ unless then_form
152
+ raise RubyLisp::RuntimeError,
153
+ "An `if` form must at least have a 'then' branch."
154
+ end
155
+
156
+ if (eval_ast cond, env)
157
+ eval_ast(then_form, env)
158
+ elsif else_form
159
+ eval_ast(else_form, env)
160
+ end
161
+ elsif input.value[0].value == 'in-ns'
162
+ assert_number_of_args input, 1
163
+ input.value[1] = eval_ast(input.value[1], env)
164
+ assert_arg_type input, 1, RubyLisp::Symbol
165
+ ns_name = input.value[1].to_s
166
+ # TODO: register ns and switch to it
167
+ env.namespace = ns_name
168
+ elsif input.value[0].value == 'let'
169
+ assert_arg_type input, 1, RubyLisp::Vector
170
+ inner_env = RubyLisp::Environment.new(outer: env)
171
+ bindings, *body = input.value[1..-1]
172
+ unless bindings.value.count.even?
173
+ raise RubyLisp::RuntimeError,
174
+ "The bindings vector of `let` must contain an even number " +
175
+ " of forms."
176
+ end
177
+
178
+ bindings.value.each_slice(2) do |(k, v)|
179
+ inner_env.set k.value, eval_ast(v, inner_env)
180
+ end
181
+
182
+ body.map {|form| eval_ast(form, inner_env)}.last
183
+ elsif input.value[0].value == 'ns'
184
+ # ns will be defined more robustly in rbl.core, but rbl.core also
185
+ # needs the `ns` form in order to declare that it is rbl.core.
186
+ #
187
+ # defining it here in a minimal form where it is equivalent to in-ns,
188
+ # except that you don't have to quote the namespace name
189
+ assert_number_of_args input, 1
190
+ assert_arg_type input, 1, RubyLisp::Symbol
191
+ ns_name = input.value[1].to_s
192
+ # TODO: register ns and switch to it
193
+ env.namespace = ns_name
194
+ elsif input.value[0].value == 'quote'
195
+ assert_number_of_args input, 1
196
+ fn, *args = input.value
197
+ quoted_form = args[0]
198
+
199
+ if [RubyLisp::List, RubyLisp::Vector].member? quoted_form.class
200
+ quoted_form.quote.value
201
+ else
202
+ quoted_form.quote
203
+ end
204
+ elsif input.value[0].value == 'resolve'
205
+ assert_number_of_args input, 1
206
+ symbol = eval_ast(input.value[1], env)
207
+ sexp = RubyLisp::List.new(input.value.fill(symbol, 1, 1))
208
+ assert_arg_type sexp, 1, RubyLisp::Symbol
209
+ symbol.resolve(env)
210
+ else
211
+ fn, *args = input.value.map {|value| eval_ast(value, env)}
212
+ fn.call(*args)
213
+ end
214
+ when RubyLisp::Symbol
215
+ if input.quoted
216
+ input
217
+ else
218
+ input.resolve(env)
219
+ end
220
+ when RubyLisp::Vector
221
+ input.value.map {|value| eval_ast(value, env)}
222
+ else
223
+ input.value
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,28 @@
1
+ require 'rubylisp/printer'
2
+ require 'rubylisp/reader'
3
+ require 'rubylisp/environment'
4
+ require 'rubylisp/evaluator'
5
+
6
+ module RubyLisp
7
+ module Parser
8
+ module_function
9
+
10
+ def read input
11
+ RubyLisp::Reader.read_str input
12
+ end
13
+
14
+ def eval_ast input, env
15
+ RubyLisp::Evaluator.eval_ast input, env
16
+ end
17
+
18
+ def print input
19
+ RubyLisp::Printer.pr_str input
20
+ end
21
+
22
+ def parse input, env = RubyLisp::Environment.new.stdlib
23
+ ast = read input
24
+ result = eval_ast ast, env
25
+ print result
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'rubylisp/types'
2
+
3
+ module RubyLisp
4
+ module Printer
5
+ module_function
6
+
7
+ def pr_str(*xs)
8
+ if xs.count > 1
9
+ xs.map {|x| pr_str(x)}.join(' ')
10
+ else
11
+ x = xs.first
12
+ case x
13
+ when Array, Hamster::Vector
14
+ "[#{x.map {|item| pr_str(item)}.join(' ')}]"
15
+ when Hash
16
+ "{#{x.map {|k, v| "#{pr_str(k)} #{pr_str(v)}"}.join ", "}}"
17
+ when Hamster::Hash
18
+ pr_str x.to_hash
19
+ when Hamster::List
20
+ "(#{x.map {|item| pr_str(item)}.join(' ')})"
21
+ when RubyLisp::Keyword
22
+ ":#{x.value}"
23
+ when RubyLisp::List
24
+ "(#{x.value.map {|item| pr_str(item)}.join(' ')})"
25
+ when RubyLisp::Symbol, RubyLisp::Int, RubyLisp::Float
26
+ x.value
27
+ else
28
+ x.inspect
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end